gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/testutil/testutil.go (about) 1 // Copyright 2018 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package testutil contains utility functions for runsc tests. 16 package testutil 17 18 import ( 19 "bufio" 20 "context" 21 "debug/elf" 22 "encoding/base32" 23 "encoding/json" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "log" 28 "math" 29 "math/rand" 30 "net/http" 31 "os" 32 "os/exec" 33 "os/signal" 34 "path" 35 "path/filepath" 36 "strconv" 37 "strings" 38 "testing" 39 "time" 40 41 "github.com/cenkalti/backoff" 42 specs "github.com/opencontainers/runtime-spec/specs-go" 43 "golang.org/x/sys/unix" 44 "gvisor.dev/gvisor/pkg/sentry/watchdog" 45 "gvisor.dev/gvisor/pkg/sync" 46 "gvisor.dev/gvisor/runsc/config" 47 "gvisor.dev/gvisor/runsc/flag" 48 "gvisor.dev/gvisor/runsc/specutils" 49 ) 50 51 var ( 52 partition = flag.Int("partition", IntFromEnv("PARTITION", 1), "partition number, this is 1-indexed") 53 totalPartitions = flag.Int("total_partitions", IntFromEnv("TOTAL_PARTITIONS", 1), "total number of partitions") 54 runscPath = flag.String("runsc", os.Getenv("RUNTIME"), "path to runsc binary") 55 56 // Flags controlling features for sandbox under test, prefixed with 57 // "test-" to avoid potential conflicts with runsc flags. 58 checkpointSupported = flag.Bool("test-checkpoint", BoolFromEnv("TEST_CHECKPOINT", true), "control checkpoint/restore support") 59 isRunningWithOverlay = flag.Bool("test-overlay", BoolFromEnv("TEST_OVERLAY", false), "whether test is running with --overlay2") 60 isRunningWithNetRaw = flag.Bool("test-net-raw", BoolFromEnv("TEST_NET_RAW", false), "whether test is running with raw socket support") 61 isRunningWithHostNet = flag.Bool("test-hostnet", BoolFromEnv("TEST_HOSTNET", false), "whether test is running with hostnet") 62 63 // TestEnvSupportsNetAdmin indicates whether a test sandbox can perform 64 // all net admin tasks. Note that some test environments cannot perform 65 // some tasks despite the presence of CAP_NET_ADMIN. 66 TestEnvSupportsNetAdmin = true 67 ) 68 69 // StringFromEnv returns the value of the named environment variable, or `def` if unset/empty. 70 // It is useful for defining flags where the default value can be specified through the environment. 71 func StringFromEnv(name, def string) string { 72 str := os.Getenv(name) 73 if str == "" { 74 return def 75 } 76 return str 77 } 78 79 // IntFromEnv returns the integer value of the named environment variable, or `def` if unset/empty. 80 // It is useful for defining flags where the default value can be specified through the environment. 81 func IntFromEnv(name string, def int) int { 82 str := os.Getenv(name) 83 if str == "" { 84 return def 85 } 86 v, err := strconv.ParseInt(str, 10, 64) 87 if err != nil { 88 // N.B. This library is testonly, so a panic here is reasonable. 89 panic(fmt.Errorf("invalid environment variable %q; got %q expected integer: %w", name, str, err)) 90 } 91 return int(v) 92 } 93 94 // BoolFromEnv returns the boolean value of the named environment variable, or `def` if unset/empty. 95 // It is useful for defining flags where the default value can be specified through the environment. 96 func BoolFromEnv(name string, def bool) bool { 97 str := strings.ToLower(os.Getenv(name)) 98 if str == "" { 99 return def 100 } 101 v, err := strconv.ParseBool(str) 102 if err != nil { 103 panic(fmt.Errorf("invalid environment variable %q; got %q expected bool: %w", name, str, err)) 104 } 105 return v 106 } 107 108 // DurationFromEnv returns the duration of the named environment variable, or `def` if unset/empty. 109 // It is useful for defining flags where the default value can be specified through the environment. 110 func DurationFromEnv(name string, def time.Duration) time.Duration { 111 str := strings.ToLower(os.Getenv(name)) 112 if str == "" { 113 return def 114 } 115 d, err := time.ParseDuration(str) 116 if err != nil { 117 panic(fmt.Errorf("invalid environment variable %q; got %q expected duration: %w", name, str, err)) 118 } 119 return d 120 } 121 122 // IsCheckpointSupported returns the relevant command line flag. 123 func IsCheckpointSupported() bool { 124 return *checkpointSupported 125 } 126 127 // IsRunningWithHostNet returns the relevant command line flag. 128 func IsRunningWithHostNet() bool { 129 return *isRunningWithHostNet 130 } 131 132 // IsRunningWithNetRaw returns the relevant command line flag. 133 func IsRunningWithNetRaw() bool { 134 return *isRunningWithNetRaw 135 } 136 137 // IsRunningWithOverlay returns the relevant command line flag. 138 func IsRunningWithOverlay() bool { 139 return *isRunningWithOverlay 140 } 141 142 // ImageByName mangles the image name used locally. This depends on the image 143 // build infrastructure in images/ and tools/vm. 144 func ImageByName(name string) string { 145 return fmt.Sprintf("gvisor.dev/images/%s", name) 146 } 147 148 // ConfigureExePath configures the executable for runsc in the test environment. 149 func ConfigureExePath() error { 150 if *runscPath == "" { 151 path, err := FindFile("runsc/runsc") 152 if err != nil { 153 return err 154 } 155 *runscPath = path 156 } 157 specutils.ExePath = *runscPath 158 return nil 159 } 160 161 // TmpDir returns the absolute path to a writable directory that can be used as 162 // scratch by the test. 163 func TmpDir() string { 164 if dir, ok := os.LookupEnv("TEST_TMPDIR"); ok { 165 return dir 166 } 167 return "/tmp" 168 } 169 170 // Logger is a simple logging wrapper. 171 // 172 // This is designed to be implemented by *testing.T. 173 type Logger interface { 174 Name() string 175 Logf(fmt string, args ...any) 176 } 177 178 // DefaultLogger logs using the log package. 179 type DefaultLogger string 180 181 // Name implements Logger.Name. 182 func (d DefaultLogger) Name() string { 183 return string(d) 184 } 185 186 // Logf implements Logger.Logf. 187 func (d DefaultLogger) Logf(fmt string, args ...any) { 188 log.Printf(fmt, args...) 189 } 190 191 // multiLogger logs to multiple Loggers. 192 type multiLogger []Logger 193 194 // Name implements Logger.Name. 195 func (m multiLogger) Name() string { 196 names := make([]string, len(m)) 197 for i, l := range m { 198 names[i] = l.Name() 199 } 200 return strings.Join(names, "+") 201 } 202 203 // Logf implements Logger.Logf. 204 func (m multiLogger) Logf(fmt string, args ...any) { 205 for _, l := range m { 206 l.Logf(fmt, args...) 207 } 208 } 209 210 // NewMultiLogger returns a new Logger that logs on multiple Loggers. 211 func NewMultiLogger(loggers ...Logger) Logger { 212 return multiLogger(loggers) 213 } 214 215 // Cmd is a simple wrapper. 216 type Cmd struct { 217 logger Logger 218 *exec.Cmd 219 } 220 221 // CombinedOutput returns the output and logs. 222 func (c *Cmd) CombinedOutput() ([]byte, error) { 223 out, err := c.Cmd.CombinedOutput() 224 if len(out) > 0 { 225 c.logger.Logf("output: %s", string(out)) 226 } 227 if err != nil { 228 c.logger.Logf("error: %v", err) 229 } 230 return out, err 231 } 232 233 // Command is a simple wrapper around exec.Command, that logs. 234 func Command(logger Logger, args ...string) *Cmd { 235 logger.Logf("command: %s", strings.Join(args, " ")) 236 return &Cmd{ 237 logger: logger, 238 Cmd: exec.Command(args[0], args[1:]...), 239 } 240 } 241 242 // TestConfig returns the default configuration to use in tests. Note that 243 // 'RootDir' must be set by caller if required. 244 func TestConfig(t *testing.T) *config.Config { 245 logDir := os.TempDir() 246 if dir, ok := os.LookupEnv("TEST_UNDECLARED_OUTPUTS_DIR"); ok { 247 logDir = dir + "/" 248 } 249 250 testFlags := flag.NewFlagSet("test", flag.ContinueOnError) 251 config.RegisterFlags(testFlags) 252 conf, err := config.NewFromFlags(testFlags) 253 if err != nil { 254 t.Fatalf("error loading configuration from flags: %v", err) 255 } 256 // Change test defaults. 257 conf.Debug = true 258 conf.DebugLog = path.Join(logDir, "runsc.log."+t.Name()+".%TIMESTAMP%.%COMMAND%.txt") 259 conf.LogPackets = true 260 conf.Network = config.NetworkNone 261 conf.Strace = true 262 conf.TestOnlyAllowRunAsCurrentUserWithoutChroot = true 263 conf.WatchdogAction = watchdog.Panic 264 return conf 265 } 266 267 // NewSpecWithArgs creates a simple spec with the given args suitable for use 268 // in tests. 269 func NewSpecWithArgs(args ...string) *specs.Spec { 270 return &specs.Spec{ 271 // The host filesystem root is the container root. 272 Root: &specs.Root{ 273 Path: "/", 274 Readonly: true, 275 }, 276 Process: &specs.Process{ 277 Args: args, 278 Env: []string{ 279 "PATH=" + os.Getenv("PATH"), 280 }, 281 Capabilities: specutils.AllCapabilities(), 282 }, 283 Mounts: []specs.Mount{ 284 // Hide the host /etc to avoid any side-effects. 285 // For example, bash reads /etc/passwd and if it is 286 // very big, tests can fail by timeout. 287 { 288 Type: "tmpfs", 289 Destination: "/etc", 290 }, 291 // Root is readonly, but many tests want to write to tmpdir. 292 // This creates a writable mount inside the root. Also, when tmpdir points 293 // to "/tmp", it makes the actual /tmp to be mounted and not a tmpfs 294 // inside the sentry. 295 { 296 Type: "bind", 297 Destination: TmpDir(), 298 Source: TmpDir(), 299 }, 300 }, 301 Hostname: "runsc-test-hostname", 302 } 303 } 304 305 // SetupRootDir creates a root directory for containers. 306 func SetupRootDir() (string, func(), error) { 307 rootDir, err := ioutil.TempDir(TmpDir(), "containers") 308 if err != nil { 309 return "", nil, fmt.Errorf("error creating root dir: %v", err) 310 } 311 return rootDir, func() { os.RemoveAll(rootDir) }, nil 312 } 313 314 // SetupContainer creates a bundle and root dir for the container, generates a 315 // test config, and writes the spec to config.json in the bundle dir. 316 func SetupContainer(spec *specs.Spec, conf *config.Config) (rootDir, bundleDir string, cleanup func(), err error) { 317 rootDir, rootCleanup, err := SetupRootDir() 318 if err != nil { 319 return "", "", nil, err 320 } 321 conf.RootDir = rootDir 322 bundleDir, bundleCleanup, err := SetupBundleDir(spec) 323 if err != nil { 324 rootCleanup() 325 return "", "", nil, err 326 } 327 return rootDir, bundleDir, func() { 328 bundleCleanup() 329 rootCleanup() 330 }, err 331 } 332 333 // SetupBundleDir creates a bundle dir and writes the spec to config.json. 334 func SetupBundleDir(spec *specs.Spec) (string, func(), error) { 335 bundleDir, err := ioutil.TempDir(TmpDir(), "bundle") 336 if err != nil { 337 return "", nil, fmt.Errorf("error creating bundle dir: %v", err) 338 } 339 cleanup := func() { os.RemoveAll(bundleDir) } 340 if err := writeSpec(bundleDir, spec); err != nil { 341 cleanup() 342 return "", nil, fmt.Errorf("error writing spec: %v", err) 343 } 344 return bundleDir, cleanup, nil 345 } 346 347 // writeSpec writes the spec to disk in the given directory. 348 func writeSpec(dir string, spec *specs.Spec) error { 349 b, err := json.Marshal(spec) 350 if err != nil { 351 return err 352 } 353 return ioutil.WriteFile(filepath.Join(dir, "config.json"), b, 0755) 354 } 355 356 // idRandomSrc is a pseudo random generator used to in RandomID. 357 var idRandomSrc = rand.New(rand.NewSource(time.Now().UnixNano())) 358 359 // idRandomSrcMtx is the mutex protecting idRandomSrc.Read from being used 360 // concurrently in different goroutines. 361 var idRandomSrcMtx sync.Mutex 362 363 // RandomID returns 20 random bytes following the given prefix. 364 func RandomID(prefix string) string { 365 // Read 20 random bytes. 366 b := make([]byte, 20) 367 // Rand.Read is not safe for concurrent use. Packetimpact tests can be run in 368 // parallel now, so we have to protect the Read with a mutex. Otherwise we'll 369 // run into name conflicts. 370 // https://golang.org/pkg/math/rand/#Rand.Read 371 idRandomSrcMtx.Lock() 372 // "[Read] always returns len(p) and a nil error." --godoc 373 if _, err := idRandomSrc.Read(b); err != nil { 374 idRandomSrcMtx.Unlock() 375 panic("rand.Read failed: " + err.Error()) 376 } 377 idRandomSrcMtx.Unlock() 378 if prefix != "" { 379 prefix = prefix + "-" 380 } 381 return fmt.Sprintf("%s%s", prefix, base32.StdEncoding.EncodeToString(b)) 382 } 383 384 // RandomContainerID generates a random container id for each test. 385 // 386 // The container id is used to create an abstract unix domain socket, which 387 // must be unique. While the container forbids creating two containers with the 388 // same name, sometimes between test runs the socket does not get cleaned up 389 // quickly enough, causing container creation to fail. 390 func RandomContainerID() string { 391 return RandomID("test-container") 392 } 393 394 // Copy copies file from src to dst. 395 func Copy(src, dst string) error { 396 in, err := os.Open(src) 397 if err != nil { 398 return err 399 } 400 defer in.Close() 401 402 st, err := in.Stat() 403 if err != nil { 404 return err 405 } 406 407 out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, st.Mode().Perm()) 408 if err != nil { 409 return err 410 } 411 defer out.Close() 412 413 // Mirror the local user's permissions across all users. This is 414 // because as we inject things into the container, the UID/GID will 415 // change. Also, the build system may generate artifacts with different 416 // modes. At the top-level (volume mapping) we have a big read-only 417 // knob that can be applied to prevent modifications. 418 // 419 // Note that this must be done via a separate Chmod call, otherwise the 420 // current process's umask will get in the way. 421 var mode os.FileMode 422 if st.Mode()&0100 != 0 { 423 mode |= 0111 424 } 425 if st.Mode()&0200 != 0 { 426 mode |= 0222 427 } 428 if st.Mode()&0400 != 0 { 429 mode |= 0444 430 } 431 if err := os.Chmod(dst, mode); err != nil { 432 return err 433 } 434 435 _, err = io.Copy(out, in) 436 return err 437 } 438 439 // Poll is a shorthand function to poll for something with given timeout. 440 func Poll(cb func() error, timeout time.Duration) error { 441 ctx, cancel := context.WithTimeout(context.Background(), timeout) 442 defer cancel() 443 return PollContext(ctx, cb) 444 } 445 446 // PollContext is like Poll, but takes a context instead of a timeout. 447 func PollContext(ctx context.Context, cb func() error) error { 448 b := backoff.WithContext(backoff.NewConstantBackOff(100*time.Millisecond), ctx) 449 return backoff.Retry(cb, b) 450 } 451 452 // WaitForHTTP tries GET requests on a port until the call succeeds or timeout. 453 func WaitForHTTP(ip string, port int, timeout time.Duration) error { 454 cb := func() error { 455 c := &http.Client{ 456 // Calculate timeout to be able to do minimum 5 attempts. 457 Timeout: timeout / 5, 458 } 459 url := fmt.Sprintf("http://%s:%d/", ip, port) 460 resp, err := c.Get(url) 461 if err != nil { 462 log.Printf("Waiting %s: %v", url, err) 463 return err 464 } 465 resp.Body.Close() 466 return nil 467 } 468 return Poll(cb, timeout) 469 } 470 471 // Reaper reaps child processes. 472 type Reaper struct { 473 // mu protects ch, which will be nil if the reaper is not running. 474 mu sync.Mutex 475 ch chan os.Signal 476 } 477 478 // Start starts reaping child processes. 479 func (r *Reaper) Start() { 480 r.mu.Lock() 481 defer r.mu.Unlock() 482 483 if r.ch != nil { 484 panic("reaper.Start called on a running reaper") 485 } 486 487 r.ch = make(chan os.Signal, 1) 488 signal.Notify(r.ch, unix.SIGCHLD) 489 490 go func() { 491 for { 492 r.mu.Lock() 493 ch := r.ch 494 r.mu.Unlock() 495 if ch == nil { 496 return 497 } 498 499 _, ok := <-ch 500 if !ok { 501 // Channel closed. 502 return 503 } 504 for { 505 cpid, _ := unix.Wait4(-1, nil, unix.WNOHANG, nil) 506 if cpid < 1 { 507 break 508 } 509 } 510 } 511 }() 512 } 513 514 // Stop stops reaping child processes. 515 func (r *Reaper) Stop() { 516 r.mu.Lock() 517 defer r.mu.Unlock() 518 519 if r.ch == nil { 520 panic("reaper.Stop called on a stopped reaper") 521 } 522 523 signal.Stop(r.ch) 524 close(r.ch) 525 r.ch = nil 526 } 527 528 // StartReaper is a helper that starts a new Reaper and returns a function to 529 // stop it. 530 func StartReaper() func() { 531 r := &Reaper{} 532 r.Start() 533 return r.Stop 534 } 535 536 // WaitUntilRead reads from the given reader until the wanted string is found 537 // or until timeout. 538 func WaitUntilRead(r io.Reader, want string, timeout time.Duration) error { 539 sc := bufio.NewScanner(r) 540 // done must be accessed atomically. A value greater than 0 indicates 541 // that the read loop can exit. 542 doneCh := make(chan bool) 543 defer close(doneCh) 544 go func() { 545 for sc.Scan() { 546 t := sc.Text() 547 if strings.Contains(t, want) { 548 doneCh <- true 549 return 550 } 551 select { 552 case <-doneCh: 553 return 554 default: 555 } 556 } 557 doneCh <- false 558 }() 559 560 select { 561 case <-time.After(timeout): 562 return fmt.Errorf("timeout waiting to read %q", want) 563 case res := <-doneCh: 564 if !res { 565 return fmt.Errorf("reader closed while waiting to read %q", want) 566 } 567 return nil 568 } 569 } 570 571 // KillCommand kills the process running cmd unless it hasn't been started. It 572 // returns an error if it cannot kill the process unless the reason is that the 573 // process has already exited. 574 // 575 // KillCommand will also reap the process. 576 func KillCommand(cmd *exec.Cmd) error { 577 if cmd.Process == nil { 578 return nil 579 } 580 if err := cmd.Process.Kill(); err != nil { 581 if !strings.Contains(err.Error(), "process already finished") { 582 return fmt.Errorf("failed to kill process %v: %v", cmd, err) 583 } 584 } 585 return cmd.Wait() 586 } 587 588 // WriteTmpFile writes text to a temporary file, closes the file, and returns 589 // the name of the file. A cleanup function is also returned. 590 func WriteTmpFile(pattern, text string) (string, func(), error) { 591 file, err := ioutil.TempFile(TmpDir(), pattern) 592 if err != nil { 593 return "", nil, err 594 } 595 defer file.Close() 596 if _, err := file.Write([]byte(text)); err != nil { 597 return "", nil, err 598 } 599 return file.Name(), func() { os.RemoveAll(file.Name()) }, nil 600 } 601 602 // IsStatic returns true iff the given file is a static binary. 603 func IsStatic(filename string) (bool, error) { 604 f, err := elf.Open(filename) 605 if err != nil { 606 return false, err 607 } 608 for _, prog := range f.Progs { 609 if prog.Type == elf.PT_INTERP { 610 return false, nil // Has interpreter. 611 } 612 } 613 return true, nil 614 } 615 616 // TouchShardStatusFile indicates to Bazel that the test runner supports 617 // sharding by creating or updating the last modified date of the file 618 // specified by TEST_SHARD_STATUS_FILE. 619 // 620 // See https://docs.bazel.build/versions/master/test-encyclopedia.html#role-of-the-test-runner. 621 func TouchShardStatusFile() error { 622 if statusFile, ok := os.LookupEnv("TEST_SHARD_STATUS_FILE"); ok { 623 cmd := exec.Command("touch", statusFile) 624 if b, err := cmd.CombinedOutput(); err != nil { 625 return fmt.Errorf("touch %q failed:\n output: %s\n error: %s", statusFile, string(b), err.Error()) 626 } 627 } 628 return nil 629 } 630 631 // TestIndicesForShard returns indices for this test shard based on the 632 // TEST_SHARD_INDEX and TEST_TOTAL_SHARDS environment vars, as well as 633 // the passed partition flags. 634 // 635 // If either of the env vars are not present, then the function will return all 636 // tests. If there are more shards than there are tests, then the returned list 637 // may be empty. 638 func TestIndicesForShard(numTests int) ([]int, error) { 639 var ( 640 shardIndex = 0 641 shardTotal = 1 642 ) 643 644 indexStr, indexOk := os.LookupEnv("TEST_SHARD_INDEX") 645 totalStr, totalOk := os.LookupEnv("TEST_TOTAL_SHARDS") 646 if indexOk && totalOk { 647 // Parse index and total to ints. 648 var err error 649 shardIndex, err = strconv.Atoi(indexStr) 650 if err != nil { 651 return nil, fmt.Errorf("invalid TEST_SHARD_INDEX %q: %v", indexStr, err) 652 } 653 shardTotal, err = strconv.Atoi(totalStr) 654 if err != nil { 655 return nil, fmt.Errorf("invalid TEST_TOTAL_SHARDS %q: %v", totalStr, err) 656 } 657 } 658 659 // Combine with the partitions. 660 partitionSize := shardTotal 661 shardTotal = (*totalPartitions) * shardTotal 662 shardIndex = partitionSize*(*partition-1) + shardIndex 663 664 // Calculate! 665 var indices []int 666 numBlocks := int(math.Ceil(float64(numTests) / float64(shardTotal))) 667 for i := 0; i < numBlocks; i++ { 668 pick := i*shardTotal + shardIndex 669 if pick < numTests { 670 indices = append(indices, pick) 671 } 672 } 673 return indices, nil 674 }