github.com/bigcommerce/nomad@v0.9.3-bc/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 ctestutil "github.com/hashicorp/nomad/client/testutil" 17 "github.com/hashicorp/nomad/helper/pluginutils/hclutils" 18 "github.com/hashicorp/nomad/helper/testlog" 19 "github.com/hashicorp/nomad/helper/testtask" 20 "github.com/hashicorp/nomad/helper/uuid" 21 basePlug "github.com/hashicorp/nomad/plugins/base" 22 "github.com/hashicorp/nomad/plugins/drivers" 23 dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils" 24 pstructs "github.com/hashicorp/nomad/plugins/shared/structs" 25 "github.com/hashicorp/nomad/testutil" 26 "github.com/stretchr/testify/require" 27 ) 28 29 func TestMain(m *testing.M) { 30 if !testtask.Run() { 31 os.Exit(m.Run()) 32 } 33 } 34 35 func TestRawExecDriver_SetConfig(t *testing.T) { 36 t.Parallel() 37 require := require.New(t) 38 39 d := NewRawExecDriver(testlog.HCLogger(t)) 40 harness := dtestutil.NewDriverHarness(t, d) 41 defer harness.Kill() 42 43 bconfig := &basePlug.Config{} 44 45 // Disable raw exec. 46 config := &Config{} 47 48 var data []byte 49 require.NoError(basePlug.MsgPackEncode(&data, config)) 50 bconfig.PluginConfig = data 51 require.NoError(harness.SetConfig(bconfig)) 52 require.Exactly(config, d.(*Driver).config) 53 54 config.Enabled = true 55 config.NoCgroups = true 56 data = []byte{} 57 require.NoError(basePlug.MsgPackEncode(&data, config)) 58 bconfig.PluginConfig = data 59 require.NoError(harness.SetConfig(bconfig)) 60 require.Exactly(config, d.(*Driver).config) 61 62 config.NoCgroups = false 63 data = []byte{} 64 require.NoError(basePlug.MsgPackEncode(&data, config)) 65 bconfig.PluginConfig = data 66 require.NoError(harness.SetConfig(bconfig)) 67 require.Exactly(config, d.(*Driver).config) 68 } 69 70 func TestRawExecDriver_Fingerprint(t *testing.T) { 71 t.Parallel() 72 73 fingerprintTest := func(config *Config, expected *drivers.Fingerprint) func(t *testing.T) { 74 return func(t *testing.T) { 75 require := require.New(t) 76 d := NewRawExecDriver(testlog.HCLogger(t)).(*Driver) 77 harness := dtestutil.NewDriverHarness(t, d) 78 defer harness.Kill() 79 80 var data []byte 81 require.NoError(basePlug.MsgPackEncode(&data, config)) 82 bconfig := &basePlug.Config{ 83 PluginConfig: data, 84 } 85 require.NoError(harness.SetConfig(bconfig)) 86 87 fingerCh, err := harness.Fingerprint(context.Background()) 88 require.NoError(err) 89 select { 90 case result := <-fingerCh: 91 require.Equal(expected, result) 92 case <-time.After(time.Duration(testutil.TestMultiplier()) * time.Second): 93 require.Fail("timeout receiving fingerprint") 94 } 95 } 96 } 97 98 cases := []struct { 99 Name string 100 Conf Config 101 Expected drivers.Fingerprint 102 }{ 103 { 104 Name: "Disabled", 105 Conf: Config{ 106 Enabled: false, 107 }, 108 Expected: drivers.Fingerprint{ 109 Attributes: nil, 110 Health: drivers.HealthStateUndetected, 111 HealthDescription: "disabled", 112 }, 113 }, 114 { 115 Name: "Enabled", 116 Conf: Config{ 117 Enabled: true, 118 }, 119 Expected: drivers.Fingerprint{ 120 Attributes: map[string]*pstructs.Attribute{"driver.raw_exec": pstructs.NewBoolAttribute(true)}, 121 Health: drivers.HealthStateHealthy, 122 HealthDescription: drivers.DriverHealthy, 123 }, 124 }, 125 } 126 127 for _, tc := range cases { 128 t.Run(tc.Name, fingerprintTest(&tc.Conf, &tc.Expected)) 129 } 130 } 131 132 func TestRawExecDriver_StartWait(t *testing.T) { 133 t.Parallel() 134 require := require.New(t) 135 136 d := NewRawExecDriver(testlog.HCLogger(t)) 137 harness := dtestutil.NewDriverHarness(t, d) 138 defer harness.Kill() 139 task := &drivers.TaskConfig{ 140 ID: uuid.Generate(), 141 Name: "test", 142 } 143 144 tc := &TaskConfig{ 145 Command: testtask.Path(), 146 Args: []string{"sleep", "10ms"}, 147 } 148 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 149 testtask.SetTaskConfigEnv(task) 150 151 cleanup := harness.MkAllocDir(task, false) 152 defer cleanup() 153 154 handle, _, err := harness.StartTask(task) 155 require.NoError(err) 156 157 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 158 require.NoError(err) 159 160 var result *drivers.ExitResult 161 select { 162 case result = <-ch: 163 case <-time.After(5 * time.Second): 164 t.Fatal("timed out") 165 } 166 167 require.Zero(result.ExitCode) 168 require.Zero(result.Signal) 169 require.False(result.OOMKilled) 170 require.NoError(result.Err) 171 require.NoError(harness.DestroyTask(task.ID, true)) 172 } 173 174 func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) { 175 t.Parallel() 176 require := require.New(t) 177 178 d := NewRawExecDriver(testlog.HCLogger(t)) 179 harness := dtestutil.NewDriverHarness(t, d) 180 defer harness.Kill() 181 182 // Disable cgroups so test works without root 183 config := &Config{NoCgroups: true} 184 var data []byte 185 require.NoError(basePlug.MsgPackEncode(&data, config)) 186 bconfig := &basePlug.Config{PluginConfig: data} 187 require.NoError(harness.SetConfig(bconfig)) 188 189 task := &drivers.TaskConfig{ 190 ID: uuid.Generate(), 191 Name: "sleep", 192 } 193 tc := &TaskConfig{ 194 Command: testtask.Path(), 195 Args: []string{"sleep", "100s"}, 196 } 197 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 198 199 testtask.SetTaskConfigEnv(task) 200 cleanup := harness.MkAllocDir(task, false) 201 defer cleanup() 202 203 handle, _, err := harness.StartTask(task) 204 require.NoError(err) 205 206 ch, err := harness.WaitTask(context.Background(), task.ID) 207 require.NoError(err) 208 209 var waitDone bool 210 var wg sync.WaitGroup 211 wg.Add(1) 212 go func() { 213 defer wg.Done() 214 result := <-ch 215 require.Error(result.Err) 216 waitDone = true 217 }() 218 219 originalStatus, err := d.InspectTask(task.ID) 220 require.NoError(err) 221 222 d.(*Driver).tasks.Delete(task.ID) 223 224 wg.Wait() 225 require.True(waitDone) 226 _, err = d.InspectTask(task.ID) 227 require.Equal(drivers.ErrTaskNotFound, err) 228 229 err = d.RecoverTask(handle) 230 require.NoError(err) 231 232 status, err := d.InspectTask(task.ID) 233 require.NoError(err) 234 require.Exactly(originalStatus, status) 235 236 ch, err = harness.WaitTask(context.Background(), task.ID) 237 require.NoError(err) 238 239 wg.Add(1) 240 waitDone = false 241 go func() { 242 defer wg.Done() 243 result := <-ch 244 require.NoError(result.Err) 245 require.NotZero(result.ExitCode) 246 require.Equal(9, result.Signal) 247 waitDone = true 248 }() 249 250 time.Sleep(300 * time.Millisecond) 251 require.NoError(d.StopTask(task.ID, 0, "SIGKILL")) 252 wg.Wait() 253 require.NoError(d.DestroyTask(task.ID, false)) 254 require.True(waitDone) 255 } 256 257 func TestRawExecDriver_Start_Wait_AllocDir(t *testing.T) { 258 t.Parallel() 259 require := require.New(t) 260 261 d := NewRawExecDriver(testlog.HCLogger(t)) 262 harness := dtestutil.NewDriverHarness(t, d) 263 defer harness.Kill() 264 265 task := &drivers.TaskConfig{ 266 ID: uuid.Generate(), 267 Name: "sleep", 268 } 269 270 cleanup := harness.MkAllocDir(task, false) 271 defer cleanup() 272 273 exp := []byte("win") 274 file := "output.txt" 275 outPath := fmt.Sprintf(`%s/%s`, task.TaskDir().SharedAllocDir, file) 276 277 tc := &TaskConfig{ 278 Command: testtask.Path(), 279 Args: []string{"sleep", "1s", "write", string(exp), outPath}, 280 } 281 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 282 testtask.SetTaskConfigEnv(task) 283 284 _, _, err := harness.StartTask(task) 285 require.NoError(err) 286 287 // Task should terminate quickly 288 waitCh, err := harness.WaitTask(context.Background(), task.ID) 289 require.NoError(err) 290 291 select { 292 case res := <-waitCh: 293 require.NoError(res.Err) 294 require.True(res.Successful()) 295 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 296 require.Fail("WaitTask timeout") 297 } 298 299 // Check that data was written to the shared alloc directory. 300 outputFile := filepath.Join(task.TaskDir().SharedAllocDir, file) 301 act, err := ioutil.ReadFile(outputFile) 302 require.NoError(err) 303 require.Exactly(exp, act) 304 require.NoError(harness.DestroyTask(task.ID, true)) 305 } 306 307 // This test creates a process tree such that without cgroups tracking the 308 // processes cleanup of the children would not be possible. Thus the test 309 // asserts that the processes get killed properly when using cgroups. 310 func TestRawExecDriver_Start_Kill_Wait_Cgroup(t *testing.T) { 311 ctestutil.ExecCompatible(t) 312 t.Parallel() 313 require := require.New(t) 314 pidFile := "pid" 315 316 d := NewRawExecDriver(testlog.HCLogger(t)) 317 harness := dtestutil.NewDriverHarness(t, d) 318 defer harness.Kill() 319 320 task := &drivers.TaskConfig{ 321 ID: uuid.Generate(), 322 Name: "sleep", 323 User: "root", 324 } 325 326 cleanup := harness.MkAllocDir(task, false) 327 defer cleanup() 328 329 tc := &TaskConfig{ 330 Command: testtask.Path(), 331 Args: []string{"fork/exec", pidFile, "pgrp", "0", "sleep", "20s"}, 332 } 333 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 334 testtask.SetTaskConfigEnv(task) 335 336 _, _, err := harness.StartTask(task) 337 require.NoError(err) 338 339 // Find the process 340 var pidData []byte 341 testutil.WaitForResult(func() (bool, error) { 342 var err error 343 pidData, err = ioutil.ReadFile(filepath.Join(task.TaskDir().Dir, pidFile)) 344 if err != nil { 345 return false, err 346 } 347 348 if len(pidData) == 0 { 349 return false, fmt.Errorf("pidFile empty") 350 } 351 352 return true, nil 353 }, func(err error) { 354 require.NoError(err) 355 }) 356 357 pid, err := strconv.Atoi(string(pidData)) 358 require.NoError(err, "failed to read pidData: %s", string(pidData)) 359 360 // Check the pid is up 361 process, err := os.FindProcess(pid) 362 require.NoError(err) 363 require.NoError(process.Signal(syscall.Signal(0))) 364 365 var wg sync.WaitGroup 366 wg.Add(1) 367 go func() { 368 defer wg.Done() 369 time.Sleep(1 * time.Second) 370 err := harness.StopTask(task.ID, 0, "") 371 372 // Can't rely on the ordering between wait and kill on CI/travis... 373 if !testutil.IsCI() { 374 require.NoError(err) 375 } 376 }() 377 378 // Task should terminate quickly 379 waitCh, err := harness.WaitTask(context.Background(), task.ID) 380 require.NoError(err) 381 select { 382 case res := <-waitCh: 383 require.False(res.Successful()) 384 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 385 require.Fail("WaitTask timeout") 386 } 387 388 testutil.WaitForResult(func() (bool, error) { 389 if err := process.Signal(syscall.Signal(0)); err == nil { 390 return false, fmt.Errorf("process should not exist: %v", pid) 391 } 392 393 return true, nil 394 }, func(err error) { 395 require.NoError(err) 396 }) 397 398 wg.Wait() 399 require.NoError(harness.DestroyTask(task.ID, true)) 400 } 401 402 func TestRawExecDriver_Exec(t *testing.T) { 403 t.Parallel() 404 require := require.New(t) 405 406 d := NewRawExecDriver(testlog.HCLogger(t)) 407 harness := dtestutil.NewDriverHarness(t, d) 408 defer harness.Kill() 409 410 task := &drivers.TaskConfig{ 411 ID: uuid.Generate(), 412 Name: "sleep", 413 } 414 415 cleanup := harness.MkAllocDir(task, false) 416 defer cleanup() 417 418 tc := &TaskConfig{ 419 Command: testtask.Path(), 420 Args: []string{"sleep", "9000s"}, 421 } 422 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 423 testtask.SetTaskConfigEnv(task) 424 425 _, _, err := harness.StartTask(task) 426 require.NoError(err) 427 428 if runtime.GOOS == "windows" { 429 // Exec a command that should work 430 res, err := harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "echo", "hello"}, 1*time.Second) 431 require.NoError(err) 432 require.True(res.ExitResult.Successful()) 433 require.Equal(string(res.Stdout), "hello\r\n") 434 435 // Exec a command that should fail 436 res, err = harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "stat", "notarealfile123abc"}, 1*time.Second) 437 require.NoError(err) 438 require.False(res.ExitResult.Successful()) 439 require.Contains(string(res.Stdout), "not recognized") 440 } else { 441 // Exec a command that should work 442 res, err := harness.ExecTask(task.ID, []string{"/usr/bin/stat", "/tmp"}, 1*time.Second) 443 require.NoError(err) 444 require.True(res.ExitResult.Successful()) 445 require.True(len(res.Stdout) > 100) 446 447 // Exec a command that should fail 448 res, err = harness.ExecTask(task.ID, []string{"/usr/bin/stat", "notarealfile123abc"}, 1*time.Second) 449 require.NoError(err) 450 require.False(res.ExitResult.Successful()) 451 require.Contains(string(res.Stdout), "No such file or directory") 452 } 453 454 require.NoError(harness.DestroyTask(task.ID, true)) 455 } 456 457 func TestConfig_ParseAllHCL(t *testing.T) { 458 cfgStr := ` 459 config { 460 command = "/bin/bash" 461 args = ["-c", "echo hello"] 462 }` 463 464 expected := &TaskConfig{ 465 Command: "/bin/bash", 466 Args: []string{"-c", "echo hello"}, 467 } 468 469 var tc *TaskConfig 470 hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc) 471 472 require.EqualValues(t, expected, tc) 473 }