github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/drivers/shared/executor/executor_linux_test.go (about) 1 package executor 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strconv" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/hashicorp/nomad/client/allocdir" 16 "github.com/hashicorp/nomad/client/taskenv" 17 "github.com/hashicorp/nomad/client/testutil" 18 "github.com/hashicorp/nomad/helper/testlog" 19 "github.com/hashicorp/nomad/nomad/mock" 20 "github.com/hashicorp/nomad/plugins/drivers" 21 tu "github.com/hashicorp/nomad/testutil" 22 "github.com/opencontainers/runc/libcontainer/cgroups" 23 lconfigs "github.com/opencontainers/runc/libcontainer/configs" 24 "github.com/stretchr/testify/require" 25 "golang.org/x/sys/unix" 26 ) 27 28 func init() { 29 executorFactories["LibcontainerExecutor"] = libcontainerFactory 30 } 31 32 var libcontainerFactory = executorFactory{ 33 new: NewExecutorWithIsolation, 34 configureExecCmd: func(t *testing.T, cmd *ExecCommand) { 35 cmd.ResourceLimits = true 36 setupRootfs(t, cmd.TaskDir) 37 }, 38 } 39 40 // testExecutorContextWithChroot returns an ExecutorContext and AllocDir with 41 // chroot. Use testExecutorContext if you don't need a chroot. 42 // 43 // The caller is responsible for calling AllocDir.Destroy() to cleanup. 44 func testExecutorCommandWithChroot(t *testing.T) *testExecCmd { 45 chrootEnv := map[string]string{ 46 "/etc/ld.so.cache": "/etc/ld.so.cache", 47 "/etc/ld.so.conf": "/etc/ld.so.conf", 48 "/etc/ld.so.conf.d": "/etc/ld.so.conf.d", 49 "/etc/passwd": "/etc/passwd", 50 "/lib": "/lib", 51 "/lib64": "/lib64", 52 "/usr/lib": "/usr/lib", 53 "/bin/ls": "/bin/ls", 54 "/bin/cat": "/bin/cat", 55 "/bin/echo": "/bin/echo", 56 "/bin/bash": "/bin/bash", 57 "/bin/sleep": "/bin/sleep", 58 "/foobar": "/does/not/exist", 59 } 60 61 alloc := mock.Alloc() 62 task := alloc.Job.TaskGroups[0].Tasks[0] 63 taskEnv := taskenv.NewBuilder(mock.Node(), alloc, task, "global").Build() 64 65 allocDir := allocdir.NewAllocDir(testlog.HCLogger(t), filepath.Join(os.TempDir(), alloc.ID)) 66 if err := allocDir.Build(); err != nil { 67 t.Fatalf("AllocDir.Build() failed: %v", err) 68 } 69 if err := allocDir.NewTaskDir(task.Name).Build(true, chrootEnv); err != nil { 70 allocDir.Destroy() 71 t.Fatalf("allocDir.NewTaskDir(%q) failed: %v", task.Name, err) 72 } 73 td := allocDir.TaskDirs[task.Name] 74 cmd := &ExecCommand{ 75 Env: taskEnv.List(), 76 TaskDir: td.Dir, 77 Resources: &drivers.Resources{ 78 NomadResources: alloc.AllocatedResources.Tasks[task.Name], 79 }, 80 } 81 82 testCmd := &testExecCmd{ 83 command: cmd, 84 allocDir: allocDir, 85 } 86 configureTLogging(t, testCmd) 87 return testCmd 88 } 89 90 func TestExecutor_IsolationAndConstraints(t *testing.T) { 91 t.Parallel() 92 require := require.New(t) 93 testutil.ExecCompatible(t) 94 95 testExecCmd := testExecutorCommandWithChroot(t) 96 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 97 execCmd.Cmd = "/bin/ls" 98 execCmd.Args = []string{"-F", "/", "/etc/"} 99 defer allocDir.Destroy() 100 101 execCmd.ResourceLimits = true 102 103 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 104 defer executor.Shutdown("SIGKILL", 0) 105 106 ps, err := executor.Launch(execCmd) 107 require.NoError(err) 108 require.NotZero(ps.Pid) 109 110 state, err := executor.Wait(context.Background()) 111 require.NoError(err) 112 require.Zero(state.ExitCode) 113 114 // Check if the resource constraints were applied 115 if lexec, ok := executor.(*LibcontainerExecutor); ok { 116 state, err := lexec.container.State() 117 require.NoError(err) 118 119 memLimits := filepath.Join(state.CgroupPaths["memory"], "memory.limit_in_bytes") 120 data, err := ioutil.ReadFile(memLimits) 121 require.NoError(err) 122 123 expectedMemLim := strconv.Itoa(int(execCmd.Resources.NomadResources.Memory.MemoryMB * 1024 * 1024)) 124 actualMemLim := strings.TrimSpace(string(data)) 125 require.Equal(actualMemLim, expectedMemLim) 126 require.NoError(executor.Shutdown("", 0)) 127 executor.Wait(context.Background()) 128 129 // Check if Nomad has actually removed the cgroups 130 tu.WaitForResult(func() (bool, error) { 131 _, err = os.Stat(memLimits) 132 if err == nil { 133 return false, fmt.Errorf("expected an error from os.Stat %s", memLimits) 134 } 135 return true, nil 136 }, func(err error) { t.Error(err) }) 137 138 } 139 expected := `/: 140 alloc/ 141 bin/ 142 dev/ 143 etc/ 144 lib/ 145 lib64/ 146 local/ 147 proc/ 148 secrets/ 149 sys/ 150 tmp/ 151 usr/ 152 153 /etc/: 154 ld.so.cache 155 ld.so.conf 156 ld.so.conf.d/ 157 passwd` 158 tu.WaitForResult(func() (bool, error) { 159 output := testExecCmd.stdout.String() 160 act := strings.TrimSpace(string(output)) 161 if act != expected { 162 return false, fmt.Errorf("Command output incorrectly: want %v; got %v", expected, act) 163 } 164 return true, nil 165 }, func(err error) { t.Error(err) }) 166 } 167 168 // TestExecutor_CgroupPaths asserts that process starts with independent cgroups 169 // hierarchy created for this process 170 func TestExecutor_CgroupPaths(t *testing.T) { 171 t.Parallel() 172 require := require.New(t) 173 testutil.ExecCompatible(t) 174 175 testExecCmd := testExecutorCommandWithChroot(t) 176 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 177 execCmd.Cmd = "/bin/bash" 178 execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"} 179 defer allocDir.Destroy() 180 181 execCmd.ResourceLimits = true 182 183 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 184 defer executor.Shutdown("SIGKILL", 0) 185 186 ps, err := executor.Launch(execCmd) 187 require.NoError(err) 188 require.NotZero(ps.Pid) 189 190 state, err := executor.Wait(context.Background()) 191 require.NoError(err) 192 require.Zero(state.ExitCode) 193 194 tu.WaitForResult(func() (bool, error) { 195 output := strings.TrimSpace(testExecCmd.stdout.String()) 196 // sanity check that we got some cgroups 197 if !strings.Contains(output, ":devices:") { 198 return false, fmt.Errorf("was expected cgroup files but found:\n%v", output) 199 } 200 lines := strings.Split(output, "\n") 201 for _, line := range lines { 202 // Every cgroup entry should be /nomad/$ALLOC_ID 203 if line == "" { 204 continue 205 } 206 207 // Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker 208 // don't isolate it by default. 209 if strings.Contains(line, ":rdma:") { 210 continue 211 } 212 213 if !strings.Contains(line, ":/nomad/") { 214 return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line) 215 } 216 } 217 return true, nil 218 }, func(err error) { t.Error(err) }) 219 } 220 221 // TestExecutor_CgroupPaths asserts that all cgroups created for a task 222 // are destroyed on shutdown 223 func TestExecutor_CgroupPathsAreDestroyed(t *testing.T) { 224 t.Parallel() 225 require := require.New(t) 226 testutil.ExecCompatible(t) 227 228 testExecCmd := testExecutorCommandWithChroot(t) 229 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 230 execCmd.Cmd = "/bin/bash" 231 execCmd.Args = []string{"-c", "sleep 0.2; cat /proc/self/cgroup"} 232 defer allocDir.Destroy() 233 234 execCmd.ResourceLimits = true 235 236 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 237 defer executor.Shutdown("SIGKILL", 0) 238 239 ps, err := executor.Launch(execCmd) 240 require.NoError(err) 241 require.NotZero(ps.Pid) 242 243 state, err := executor.Wait(context.Background()) 244 require.NoError(err) 245 require.Zero(state.ExitCode) 246 247 var cgroupsPaths string 248 tu.WaitForResult(func() (bool, error) { 249 output := strings.TrimSpace(testExecCmd.stdout.String()) 250 // sanity check that we got some cgroups 251 if !strings.Contains(output, ":devices:") { 252 return false, fmt.Errorf("was expected cgroup files but found:\n%v", output) 253 } 254 lines := strings.Split(output, "\n") 255 for _, line := range lines { 256 // Every cgroup entry should be /nomad/$ALLOC_ID 257 if line == "" { 258 continue 259 } 260 261 // Skip rdma subsystem; rdma was added in most recent kernels and libcontainer/docker 262 // don't isolate it by default. 263 if strings.Contains(line, ":rdma:") { 264 continue 265 } 266 267 if !strings.Contains(line, ":/nomad/") { 268 return false, fmt.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line) 269 } 270 } 271 272 cgroupsPaths = output 273 return true, nil 274 }, func(err error) { t.Error(err) }) 275 276 // shutdown executor and test that cgroups are destroyed 277 executor.Shutdown("SIGKILL", 0) 278 279 // test that the cgroup paths are not visible 280 tmpFile, err := ioutil.TempFile("", "") 281 require.NoError(err) 282 defer os.Remove(tmpFile.Name()) 283 284 _, err = tmpFile.WriteString(cgroupsPaths) 285 require.NoError(err) 286 tmpFile.Close() 287 288 subsystems, err := cgroups.ParseCgroupFile(tmpFile.Name()) 289 require.NoError(err) 290 291 for subsystem, cgroup := range subsystems { 292 if !strings.Contains(cgroup, "nomad/") { 293 // this should only be rdma at this point 294 continue 295 } 296 297 p, err := getCgroupPathHelper(subsystem, cgroup) 298 require.NoError(err) 299 require.Falsef(cgroups.PathExists(p), "cgroup for %s %s still exists", subsystem, cgroup) 300 } 301 } 302 303 func TestUniversalExecutor_LookupTaskBin(t *testing.T) { 304 t.Parallel() 305 require := require.New(t) 306 307 // Create a temp dir 308 tmpDir, err := ioutil.TempDir("", "") 309 require.Nil(err) 310 defer os.Remove(tmpDir) 311 312 // Create the command 313 cmd := &ExecCommand{Env: []string{"PATH=/bin"}, TaskDir: tmpDir} 314 315 // Make a foo subdir 316 os.MkdirAll(filepath.Join(tmpDir, "foo"), 0700) 317 318 // Write a file under foo 319 filePath := filepath.Join(tmpDir, "foo", "tmp.txt") 320 err = ioutil.WriteFile(filePath, []byte{1, 2}, os.ModeAppend) 321 require.NoError(err) 322 323 // Lookout with an absolute path to the binary 324 cmd.Cmd = "/foo/tmp.txt" 325 _, err = lookupTaskBin(cmd) 326 require.NoError(err) 327 328 // Write a file under local subdir 329 os.MkdirAll(filepath.Join(tmpDir, "local"), 0700) 330 filePath2 := filepath.Join(tmpDir, "local", "tmp.txt") 331 ioutil.WriteFile(filePath2, []byte{1, 2}, os.ModeAppend) 332 333 // Lookup with file name, should find the one we wrote above 334 cmd.Cmd = "tmp.txt" 335 _, err = lookupTaskBin(cmd) 336 require.NoError(err) 337 338 // Lookup a host absolute path 339 cmd.Cmd = "/bin/sh" 340 _, err = lookupTaskBin(cmd) 341 require.Error(err) 342 } 343 344 // Exec Launch looks for the binary only inside the chroot 345 func TestExecutor_EscapeContainer(t *testing.T) { 346 t.Parallel() 347 require := require.New(t) 348 testutil.ExecCompatible(t) 349 350 testExecCmd := testExecutorCommandWithChroot(t) 351 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 352 execCmd.Cmd = "/bin/kill" // missing from the chroot container 353 defer allocDir.Destroy() 354 355 execCmd.ResourceLimits = true 356 357 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 358 defer executor.Shutdown("SIGKILL", 0) 359 360 _, err := executor.Launch(execCmd) 361 require.Error(err) 362 require.Regexp("^file /bin/kill not found under path", err) 363 364 // Bare files are looked up using the system path, inside the container 365 allocDir.Destroy() 366 testExecCmd = testExecutorCommandWithChroot(t) 367 execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir 368 execCmd.Cmd = "kill" 369 _, err = executor.Launch(execCmd) 370 require.Error(err) 371 require.Regexp("^file kill not found under path", err) 372 373 allocDir.Destroy() 374 testExecCmd = testExecutorCommandWithChroot(t) 375 execCmd, allocDir = testExecCmd.command, testExecCmd.allocDir 376 execCmd.Cmd = "echo" 377 _, err = executor.Launch(execCmd) 378 require.NoError(err) 379 } 380 381 func TestExecutor_Capabilities(t *testing.T) { 382 t.Parallel() 383 testutil.ExecCompatible(t) 384 385 cases := []struct { 386 user string 387 caps string 388 }{ 389 { 390 user: "nobody", 391 caps: ` 392 CapInh: 0000000000000000 393 CapPrm: 0000000000000000 394 CapEff: 0000000000000000 395 CapBnd: 0000003fffffffff 396 CapAmb: 0000000000000000`, 397 }, 398 { 399 user: "root", 400 caps: ` 401 CapInh: 0000000000000000 402 CapPrm: 0000003fffffffff 403 CapEff: 0000003fffffffff 404 CapBnd: 0000003fffffffff 405 CapAmb: 0000000000000000`, 406 }, 407 } 408 409 for _, c := range cases { 410 t.Run(c.user, func(t *testing.T) { 411 require := require.New(t) 412 413 testExecCmd := testExecutorCommandWithChroot(t) 414 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 415 defer allocDir.Destroy() 416 417 execCmd.User = c.user 418 execCmd.ResourceLimits = true 419 execCmd.Cmd = "/bin/bash" 420 execCmd.Args = []string{"-c", "cat /proc/$$/status"} 421 422 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 423 defer executor.Shutdown("SIGKILL", 0) 424 425 _, err := executor.Launch(execCmd) 426 require.NoError(err) 427 428 ch := make(chan interface{}) 429 go func() { 430 executor.Wait(context.Background()) 431 close(ch) 432 }() 433 434 select { 435 case <-ch: 436 // all good 437 case <-time.After(5 * time.Second): 438 require.Fail("timeout waiting for exec to shutdown") 439 } 440 441 canonical := func(s string) string { 442 s = strings.TrimSpace(s) 443 s = regexp.MustCompile("[ \t]+").ReplaceAllString(s, " ") 444 s = regexp.MustCompile("[\n\r]+").ReplaceAllString(s, "\n") 445 return s 446 } 447 448 expected := canonical(c.caps) 449 tu.WaitForResult(func() (bool, error) { 450 output := canonical(testExecCmd.stdout.String()) 451 if !strings.Contains(output, expected) { 452 return false, fmt.Errorf("capabilities didn't match: want\n%v\n; got:\n%v\n", expected, output) 453 } 454 return true, nil 455 }, func(err error) { require.NoError(err) }) 456 }) 457 } 458 459 } 460 461 func TestExecutor_ClientCleanup(t *testing.T) { 462 t.Parallel() 463 testutil.ExecCompatible(t) 464 require := require.New(t) 465 466 testExecCmd := testExecutorCommandWithChroot(t) 467 execCmd, allocDir := testExecCmd.command, testExecCmd.allocDir 468 defer allocDir.Destroy() 469 470 executor := NewExecutorWithIsolation(testlog.HCLogger(t)) 471 defer executor.Shutdown("", 0) 472 473 // Need to run a command which will produce continuous output but not 474 // too quickly to ensure executor.Exit() stops the process. 475 execCmd.Cmd = "/bin/bash" 476 execCmd.Args = []string{"-c", "while true; do /bin/echo X; /bin/sleep 1; done"} 477 execCmd.ResourceLimits = true 478 479 ps, err := executor.Launch(execCmd) 480 481 require.NoError(err) 482 require.NotZero(ps.Pid) 483 time.Sleep(500 * time.Millisecond) 484 require.NoError(executor.Shutdown("SIGINT", 100*time.Millisecond)) 485 486 ch := make(chan interface{}) 487 go func() { 488 executor.Wait(context.Background()) 489 close(ch) 490 }() 491 492 select { 493 case <-ch: 494 // all good 495 case <-time.After(5 * time.Second): 496 require.Fail("timeout waiting for exec to shutdown") 497 } 498 499 output := testExecCmd.stdout.String() 500 require.NotZero(len(output)) 501 time.Sleep(2 * time.Second) 502 output1 := testExecCmd.stdout.String() 503 require.Equal(len(output), len(output1)) 504 } 505 506 func TestExecutor_cmdDevices(t *testing.T) { 507 input := []*drivers.DeviceConfig{ 508 { 509 HostPath: "/dev/null", 510 TaskPath: "/task/dev/null", 511 Permissions: "rwm", 512 }, 513 } 514 515 expected := &lconfigs.Device{ 516 Path: "/task/dev/null", 517 Type: 99, 518 Major: 1, 519 Minor: 3, 520 Permissions: "rwm", 521 } 522 523 found, err := cmdDevices(input) 524 require.NoError(t, err) 525 require.Len(t, found, 1) 526 527 // ignore file permission and ownership 528 // as they are host specific potentially 529 d := found[0] 530 d.FileMode = 0 531 d.Uid = 0 532 d.Gid = 0 533 534 require.EqualValues(t, expected, d) 535 } 536 537 func TestExecutor_cmdMounts(t *testing.T) { 538 input := []*drivers.MountConfig{ 539 { 540 HostPath: "/host/path-ro", 541 TaskPath: "/task/path-ro", 542 Readonly: true, 543 }, 544 { 545 HostPath: "/host/path-rw", 546 TaskPath: "/task/path-rw", 547 Readonly: false, 548 }, 549 } 550 551 expected := []*lconfigs.Mount{ 552 { 553 Source: "/host/path-ro", 554 Destination: "/task/path-ro", 555 Flags: unix.MS_BIND | unix.MS_RDONLY, 556 Device: "bind", 557 PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC}, 558 }, 559 { 560 Source: "/host/path-rw", 561 Destination: "/task/path-rw", 562 Flags: unix.MS_BIND, 563 Device: "bind", 564 PropagationFlags: []int{unix.MS_PRIVATE | unix.MS_REC}, 565 }, 566 } 567 568 require.EqualValues(t, expected, cmdMounts(input)) 569 }