github.com/moby/docker@v26.1.3+incompatible/testutil/daemon/daemon.go (about) 1 package daemon // import "github.com/docker/docker/testutil/daemon" 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "io" 8 "net/http" 9 "os" 10 "os/exec" 11 "os/user" 12 "path/filepath" 13 "strconv" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/docker/docker/api/types/events" 19 "github.com/docker/docker/api/types/system" 20 "github.com/docker/docker/client" 21 "github.com/docker/docker/container" 22 "github.com/docker/docker/pkg/ioutils" 23 "github.com/docker/docker/pkg/stringid" 24 "github.com/docker/docker/pkg/tailfile" 25 "github.com/docker/docker/testutil/request" 26 "github.com/docker/go-connections/sockets" 27 "github.com/docker/go-connections/tlsconfig" 28 "github.com/pkg/errors" 29 "gotest.tools/v3/assert" 30 "gotest.tools/v3/poll" 31 ) 32 33 // LogT is the subset of the testing.TB interface used by the daemon. 34 type LogT interface { 35 Logf(string, ...interface{}) 36 } 37 38 // nopLog is a no-op implementation of LogT that is used in daemons created by 39 // NewDaemon (where no testing.TB is available). 40 type nopLog struct{} 41 42 func (nopLog) Logf(string, ...interface{}) {} 43 44 const ( 45 defaultDockerdBinary = "dockerd" 46 defaultContainerdSocket = "/var/run/docker/containerd/containerd.sock" 47 defaultDockerdRootlessBinary = "dockerd-rootless.sh" 48 defaultUnixSocket = "/var/run/docker.sock" 49 defaultTLSHost = "localhost:2376" 50 ) 51 52 var errDaemonNotStarted = errors.New("daemon not started") 53 54 // SockRoot holds the path of the default docker integration daemon socket 55 var SockRoot = filepath.Join(os.TempDir(), "docker-integration") 56 57 type clientConfig struct { 58 transport *http.Transport 59 scheme string 60 addr string 61 } 62 63 // Daemon represents a Docker daemon for the testing framework 64 type Daemon struct { 65 Root string 66 Folder string 67 Wait chan error 68 UseDefaultHost bool 69 UseDefaultTLSHost bool 70 71 id string 72 logFile *os.File 73 cmd *exec.Cmd 74 storageDriver string 75 userlandProxy bool 76 defaultCgroupNamespaceMode string 77 execRoot string 78 experimental bool 79 init bool 80 dockerdBinary string 81 log LogT 82 pidFile string 83 args []string 84 extraEnv []string 85 containerdSocket string 86 usernsRemap string 87 rootlessUser *user.User 88 rootlessXDGRuntimeDir string 89 90 // swarm related field 91 swarmListenAddr string 92 SwarmPort int // FIXME(vdemeester) should probably not be exported 93 DefaultAddrPool []string 94 SubnetSize uint32 95 DataPathPort uint32 96 OOMScoreAdjust int 97 // cached information 98 CachedInfo system.Info 99 } 100 101 // NewDaemon returns a Daemon instance to be used for testing. 102 // The daemon will not automatically start. 103 // The daemon will modify and create files under workingDir. 104 func NewDaemon(workingDir string, ops ...Option) (*Daemon, error) { 105 storageDriver := os.Getenv("DOCKER_GRAPHDRIVER") 106 107 if err := os.MkdirAll(SockRoot, 0o700); err != nil { 108 return nil, errors.Wrapf(err, "failed to create daemon socket root %q", SockRoot) 109 } 110 111 id := "d" + stringid.TruncateID(stringid.GenerateRandomID()) 112 dir := filepath.Join(workingDir, id) 113 daemonFolder, err := filepath.Abs(dir) 114 if err != nil { 115 return nil, err 116 } 117 daemonRoot := filepath.Join(daemonFolder, "root") 118 if err := os.MkdirAll(daemonRoot, 0o755); err != nil { 119 return nil, errors.Wrapf(err, "failed to create daemon root %q", daemonRoot) 120 } 121 122 userlandProxy := true 123 if env := os.Getenv("DOCKER_USERLANDPROXY"); env != "" { 124 if val, err := strconv.ParseBool(env); err != nil { 125 userlandProxy = val 126 } 127 } 128 d := &Daemon{ 129 id: id, 130 Folder: daemonFolder, 131 Root: daemonRoot, 132 storageDriver: storageDriver, 133 userlandProxy: userlandProxy, 134 // dxr stands for docker-execroot (shortened for avoiding unix(7) path length limitation) 135 execRoot: filepath.Join(os.TempDir(), "dxr", id), 136 dockerdBinary: defaultDockerdBinary, 137 swarmListenAddr: defaultSwarmListenAddr, 138 SwarmPort: DefaultSwarmPort, 139 log: nopLog{}, 140 containerdSocket: defaultContainerdSocket, 141 } 142 143 for _, op := range ops { 144 op(d) 145 } 146 147 if d.rootlessUser != nil { 148 if err := os.Chmod(SockRoot, 0o777); err != nil { 149 return nil, err 150 } 151 uid, err := strconv.Atoi(d.rootlessUser.Uid) 152 if err != nil { 153 return nil, err 154 } 155 gid, err := strconv.Atoi(d.rootlessUser.Gid) 156 if err != nil { 157 return nil, err 158 } 159 if err := os.Chown(d.Folder, uid, gid); err != nil { 160 return nil, err 161 } 162 if err := os.Chown(d.Root, uid, gid); err != nil { 163 return nil, err 164 } 165 if err := os.MkdirAll(filepath.Dir(d.execRoot), 0o700); err != nil { 166 return nil, err 167 } 168 if err := os.Chown(filepath.Dir(d.execRoot), uid, gid); err != nil { 169 return nil, err 170 } 171 if err := os.MkdirAll(d.execRoot, 0o700); err != nil { 172 return nil, err 173 } 174 if err := os.Chown(d.execRoot, uid, gid); err != nil { 175 return nil, err 176 } 177 // $XDG_RUNTIME_DIR mustn't be too long, as ${XDG_RUNTIME_DIR/dockerd-rootless 178 // contains Unix sockets 179 d.rootlessXDGRuntimeDir = filepath.Join(os.TempDir(), "xdgrun-"+id) 180 if err := os.MkdirAll(d.rootlessXDGRuntimeDir, 0o700); err != nil { 181 return nil, err 182 } 183 if err := os.Chown(d.rootlessXDGRuntimeDir, uid, gid); err != nil { 184 return nil, err 185 } 186 d.containerdSocket = "" 187 } 188 189 return d, nil 190 } 191 192 // New returns a Daemon instance to be used for testing. 193 // This will create a directory such as d123456789 in the folder specified by 194 // $DOCKER_INTEGRATION_DAEMON_DEST or $DEST. 195 // The daemon will not automatically start. 196 func New(t testing.TB, ops ...Option) *Daemon { 197 t.Helper() 198 dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST") 199 if dest == "" { 200 dest = os.Getenv("DEST") 201 } 202 dest = filepath.Join(dest, t.Name()) 203 204 assert.Check(t, dest != "", "Please set the DOCKER_INTEGRATION_DAEMON_DEST or the DEST environment variable") 205 206 if os.Getenv("DOCKER_ROOTLESS") != "" { 207 if os.Getenv("DOCKER_REMAP_ROOT") != "" { 208 t.Skip("DOCKER_ROOTLESS doesn't support DOCKER_REMAP_ROOT currently") 209 } 210 if env := os.Getenv("DOCKER_USERLANDPROXY"); env != "" { 211 if val, err := strconv.ParseBool(env); err == nil && !val { 212 t.Skip("DOCKER_ROOTLESS doesn't support DOCKER_USERLANDPROXY=false") 213 } 214 } 215 ops = append(ops, WithRootlessUser("unprivilegeduser")) 216 } 217 ops = append(ops, WithOOMScoreAdjust(-500)) 218 219 d, err := NewDaemon(dest, ops...) 220 assert.NilError(t, err, "could not create daemon at %q", dest) 221 if d.rootlessUser != nil && d.dockerdBinary != defaultDockerdBinary { 222 t.Skipf("DOCKER_ROOTLESS doesn't support specifying non-default dockerd binary path %q", d.dockerdBinary) 223 } 224 225 return d 226 } 227 228 // BinaryPath returns the binary and its arguments. 229 func (d *Daemon) BinaryPath() (string, error) { 230 dockerdBinary, err := exec.LookPath(d.dockerdBinary) 231 if err != nil { 232 return "", errors.Wrapf(err, "[%s] could not find docker binary in $PATH", d.id) 233 } 234 return dockerdBinary, nil 235 } 236 237 // ContainersNamespace returns the containerd namespace used for containers. 238 func (d *Daemon) ContainersNamespace() string { 239 return d.id 240 } 241 242 // RootDir returns the root directory of the daemon. 243 func (d *Daemon) RootDir() string { 244 return d.Root 245 } 246 247 // ID returns the generated id of the daemon 248 func (d *Daemon) ID() string { 249 return d.id 250 } 251 252 // StorageDriver returns the configured storage driver of the daemon 253 func (d *Daemon) StorageDriver() string { 254 return d.storageDriver 255 } 256 257 // Sock returns the socket path of the daemon 258 func (d *Daemon) Sock() string { 259 return "unix://" + d.sockPath() 260 } 261 262 func (d *Daemon) sockPath() string { 263 return filepath.Join(SockRoot, d.id+".sock") 264 } 265 266 // LogFileName returns the path the daemon's log file 267 func (d *Daemon) LogFileName() string { 268 return d.logFile.Name() 269 } 270 271 // ReadLogFile returns the content of the daemon log file 272 func (d *Daemon) ReadLogFile() ([]byte, error) { 273 _ = d.logFile.Sync() 274 return os.ReadFile(d.logFile.Name()) 275 } 276 277 // NewClientT creates new client based on daemon's socket path 278 func (d *Daemon) NewClientT(t testing.TB, extraOpts ...client.Opt) *client.Client { 279 t.Helper() 280 281 c, err := d.NewClient(extraOpts...) 282 assert.NilError(t, err, "[%s] could not create daemon client", d.id) 283 t.Cleanup(func() { c.Close() }) 284 return c 285 } 286 287 // NewClient creates new client based on daemon's socket path 288 func (d *Daemon) NewClient(extraOpts ...client.Opt) (*client.Client, error) { 289 clientOpts := []client.Opt{ 290 client.FromEnv, 291 client.WithHost(d.Sock()), 292 } 293 clientOpts = append(clientOpts, extraOpts...) 294 295 return client.NewClientWithOpts(clientOpts...) 296 } 297 298 // Cleanup cleans the daemon files : exec root (network namespaces, ...), swarmkit files 299 func (d *Daemon) Cleanup(t testing.TB) { 300 t.Helper() 301 cleanupMount(t, d) 302 cleanupRaftDir(t, d) 303 cleanupDaemonStorage(t, d) 304 cleanupNetworkNamespace(t, d) 305 } 306 307 // TailLogsT attempts to tail N lines from the daemon logs. 308 // If there is an error the error is only logged, it does not cause an error with the test. 309 func (d *Daemon) TailLogsT(t LogT, n int) { 310 lines, err := d.TailLogs(n) 311 if err != nil { 312 t.Logf("[%s] %v", d.id, err) 313 return 314 } 315 for _, l := range lines { 316 t.Logf("[%s] %s", d.id, string(l)) 317 } 318 } 319 320 // PollCheckLogs is a poll.Check that checks the daemon logs using the passed in match function. 321 func (d *Daemon) PollCheckLogs(ctx context.Context, match func(s string) bool) poll.Check { 322 return func(t poll.LogT) poll.Result { 323 ok, _, err := d.ScanLogs(ctx, match) 324 if err != nil { 325 return poll.Error(err) 326 } 327 if !ok { 328 return poll.Continue("waiting for daemon logs match") 329 } 330 return poll.Success() 331 } 332 } 333 334 // ScanLogsMatchString returns a function that can be used to scan the daemon logs for the passed in string (`contains`). 335 func ScanLogsMatchString(contains string) func(string) bool { 336 return func(line string) bool { 337 return strings.Contains(line, contains) 338 } 339 } 340 341 // ScanLogsMatchCount returns a function that can be used to scan the daemon logs until the passed in matcher function matches `count` times 342 func ScanLogsMatchCount(f func(string) bool, count int) func(string) bool { 343 matched := 0 344 return func(line string) bool { 345 if f(line) { 346 matched++ 347 } 348 return matched == count 349 } 350 } 351 352 // ScanLogsMatchAll returns a function that can be used to scan the daemon logs until *all* the passed in strings are matched 353 func ScanLogsMatchAll(contains ...string) func(string) bool { 354 matched := make(map[string]bool) 355 return func(line string) bool { 356 for _, c := range contains { 357 if strings.Contains(line, c) { 358 matched[c] = true 359 } 360 } 361 return len(matched) == len(contains) 362 } 363 } 364 365 // ScanLogsT uses `ScanLogs` to match the daemon logs using the passed in match function. 366 // If there is an error or the match fails, the test will fail. 367 func (d *Daemon) ScanLogsT(ctx context.Context, t testing.TB, match func(s string) bool) (bool, string) { 368 t.Helper() 369 ok, line, err := d.ScanLogs(ctx, match) 370 assert.NilError(t, err) 371 return ok, line 372 } 373 374 // ScanLogs scans the daemon logs and passes each line to the match function. 375 func (d *Daemon) ScanLogs(ctx context.Context, match func(s string) bool) (bool, string, error) { 376 stat, err := d.logFile.Stat() 377 if err != nil { 378 return false, "", err 379 } 380 rdr := io.NewSectionReader(d.logFile, 0, stat.Size()) 381 382 scanner := bufio.NewScanner(rdr) 383 for scanner.Scan() { 384 if match(scanner.Text()) { 385 return true, scanner.Text(), nil 386 } 387 select { 388 case <-ctx.Done(): 389 return false, "", ctx.Err() 390 default: 391 } 392 } 393 return false, "", scanner.Err() 394 } 395 396 // TailLogs tails N lines from the daemon logs 397 func (d *Daemon) TailLogs(n int) ([][]byte, error) { 398 logF, err := os.Open(d.logFile.Name()) 399 if err != nil { 400 return nil, errors.Wrap(err, "error opening daemon log file after failed start") 401 } 402 403 defer logF.Close() 404 lines, err := tailfile.TailFile(logF, n) 405 if err != nil { 406 return nil, errors.Wrap(err, "error tailing log daemon logs") 407 } 408 409 return lines, nil 410 } 411 412 // Start starts the daemon and return once it is ready to receive requests. 413 func (d *Daemon) Start(t testing.TB, args ...string) { 414 t.Helper() 415 if err := d.StartWithError(args...); err != nil { 416 d.TailLogsT(t, 20) 417 d.DumpStackAndQuit() // in case the daemon is stuck 418 t.Fatalf("[%s] failed to start daemon with arguments %v : %v", d.id, d.args, err) 419 } 420 } 421 422 // StartWithError starts the daemon and return once it is ready to receive requests. 423 // It returns an error in case it couldn't start. 424 func (d *Daemon) StartWithError(args ...string) error { 425 logFile, err := os.OpenFile(filepath.Join(d.Folder, "docker.log"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600) 426 if err != nil { 427 return errors.Wrapf(err, "[%s] failed to create logfile", d.id) 428 } 429 430 return d.StartWithLogFile(logFile, args...) 431 } 432 433 // StartWithLogFile will start the daemon and attach its streams to a given file. 434 func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error { 435 d.handleUserns() 436 dockerdBinary, err := d.BinaryPath() 437 if err != nil { 438 return err 439 } 440 441 if d.pidFile == "" { 442 d.pidFile = filepath.Join(d.Folder, "docker.pid") 443 } 444 445 d.args = []string{} 446 if d.rootlessUser != nil { 447 if d.dockerdBinary != defaultDockerdBinary { 448 return errors.Errorf("[%s] DOCKER_ROOTLESS doesn't support non-default dockerd binary path %q", d.id, d.dockerdBinary) 449 } 450 dockerdBinary = "sudo" 451 d.args = append(d.args, 452 "-u", d.rootlessUser.Username, 453 "--preserve-env", 454 "--preserve-env=PATH", // Pass through PATH, overriding secure_path. 455 "XDG_RUNTIME_DIR="+d.rootlessXDGRuntimeDir, 456 "HOME="+d.rootlessUser.HomeDir, 457 "--", 458 defaultDockerdRootlessBinary, 459 ) 460 } 461 462 d.args = append(d.args, 463 "--data-root", d.Root, 464 "--exec-root", d.execRoot, 465 "--pidfile", d.pidFile, 466 "--userland-proxy="+strconv.FormatBool(d.userlandProxy), 467 "--containerd-namespace", d.id, 468 "--containerd-plugins-namespace", d.id+"p", 469 ) 470 if d.containerdSocket != "" { 471 d.args = append(d.args, "--containerd", d.containerdSocket) 472 } 473 474 if d.usernsRemap != "" { 475 d.args = append(d.args, "--userns-remap", d.usernsRemap) 476 } 477 478 if d.defaultCgroupNamespaceMode != "" { 479 d.args = append(d.args, "--default-cgroupns-mode", d.defaultCgroupNamespaceMode) 480 } 481 if d.experimental { 482 d.args = append(d.args, "--experimental") 483 } 484 if d.init { 485 d.args = append(d.args, "--init") 486 } 487 if !(d.UseDefaultHost || d.UseDefaultTLSHost) { 488 d.args = append(d.args, "--host", d.Sock()) 489 } 490 if root := os.Getenv("DOCKER_REMAP_ROOT"); root != "" { 491 d.args = append(d.args, "--userns-remap", root) 492 } 493 494 // If we don't explicitly set the log-level or debug flag(-D) then 495 // turn on debug mode 496 foundLog := false 497 foundSd := false 498 for _, a := range providedArgs { 499 if strings.Contains(a, "--log-level") || strings.Contains(a, "-D") || strings.Contains(a, "--debug") { 500 foundLog = true 501 } 502 if strings.Contains(a, "--storage-driver") { 503 foundSd = true 504 } 505 } 506 if !foundLog { 507 d.args = append(d.args, "--debug") 508 } 509 if d.storageDriver != "" && !foundSd { 510 d.args = append(d.args, "--storage-driver", d.storageDriver) 511 } 512 513 d.args = append(d.args, providedArgs...) 514 cmd := exec.Command(dockerdBinary, d.args...) 515 cmd.Env = append(os.Environ(), "DOCKER_SERVICE_PREFER_OFFLINE_IMAGE=1") 516 cmd.Env = append(cmd.Env, d.extraEnv...) 517 cmd.Env = append(cmd.Env, "OTEL_SERVICE_NAME=dockerd-"+d.id) 518 cmd.Stdout = out 519 cmd.Stderr = out 520 d.logFile = out 521 if d.rootlessUser != nil { 522 // sudo requires this for propagating signals 523 setsid(cmd) 524 } 525 526 if err := cmd.Start(); err != nil { 527 return errors.Wrapf(err, "[%s] could not start daemon container", d.id) 528 } 529 530 wait := make(chan error, 1) 531 d.cmd = cmd 532 d.Wait = wait 533 534 go func() { 535 ret := cmd.Wait() 536 d.log.Logf("[%s] exiting daemon", d.id) 537 // If we send before logging, we might accidentally log _after_ the test is done. 538 // As of Go 1.12, this incurs a panic instead of silently being dropped. 539 wait <- ret 540 close(wait) 541 }() 542 543 clientConfig, err := d.getClientConfig() 544 if err != nil { 545 return err 546 } 547 client := &http.Client{ 548 Transport: clientConfig.transport, 549 } 550 551 req, err := http.NewRequest(http.MethodGet, "/_ping", nil) 552 if err != nil { 553 return errors.Wrapf(err, "[%s] could not create new request", d.id) 554 } 555 req.URL.Host = clientConfig.addr 556 req.URL.Scheme = clientConfig.scheme 557 558 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) 559 defer cancel() 560 561 // make sure daemon is ready to receive requests 562 for i := 0; ; i++ { 563 d.log.Logf("[%s] waiting for daemon to start", d.id) 564 565 select { 566 case <-ctx.Done(): 567 return errors.Wrapf(ctx.Err(), "[%s] daemon exited and never started", d.id) 568 case err := <-d.Wait: 569 return errors.Wrapf(err, "[%s] daemon exited during startup", d.id) 570 default: 571 rctx, rcancel := context.WithTimeout(context.TODO(), 2*time.Second) 572 defer rcancel() 573 574 resp, err := client.Do(req.WithContext(rctx)) 575 if err != nil { 576 if i > 2 { // don't log the first couple, this ends up just being noise 577 d.log.Logf("[%s] error pinging daemon on start: %v", d.id, err) 578 } 579 580 select { 581 case <-ctx.Done(): 582 case <-time.After(500 * time.Millisecond): 583 } 584 continue 585 } 586 587 resp.Body.Close() 588 if resp.StatusCode != http.StatusOK { 589 d.log.Logf("[%s] received status != 200 OK: %s\n", d.id, resp.Status) 590 } 591 d.log.Logf("[%s] daemon started\n", d.id) 592 d.Root, err = d.queryRootDir() 593 if err != nil { 594 return errors.Wrapf(err, "[%s] error querying daemon for root directory", d.id) 595 } 596 return nil 597 } 598 } 599 } 600 601 // StartWithBusybox will first start the daemon with Daemon.Start() 602 // then save the busybox image from the main daemon and load it into this Daemon instance. 603 func (d *Daemon) StartWithBusybox(ctx context.Context, t testing.TB, arg ...string) { 604 t.Helper() 605 d.Start(t, arg...) 606 d.LoadBusybox(ctx, t) 607 } 608 609 // Kill will send a SIGKILL to the daemon 610 func (d *Daemon) Kill() error { 611 if d.cmd == nil || d.Wait == nil { 612 return errDaemonNotStarted 613 } 614 615 defer func() { 616 d.logFile.Close() 617 d.cmd = nil 618 }() 619 620 if err := d.cmd.Process.Kill(); err != nil { 621 return err 622 } 623 624 if d.pidFile != "" { 625 _ = os.Remove(d.pidFile) 626 } 627 return nil 628 } 629 630 // Pid returns the pid of the daemon 631 func (d *Daemon) Pid() int { 632 return d.cmd.Process.Pid 633 } 634 635 // Interrupt stops the daemon by sending it an Interrupt signal 636 func (d *Daemon) Interrupt() error { 637 return d.Signal(os.Interrupt) 638 } 639 640 // Signal sends the specified signal to the daemon if running 641 func (d *Daemon) Signal(signal os.Signal) error { 642 if d.cmd == nil || d.Wait == nil { 643 return errDaemonNotStarted 644 } 645 return d.cmd.Process.Signal(signal) 646 } 647 648 // DumpStackAndQuit sends SIGQUIT to the daemon, which triggers it to dump its 649 // stack to its log file and exit 650 // This is used primarily for gathering debug information on test timeout 651 func (d *Daemon) DumpStackAndQuit() { 652 if d.cmd == nil || d.cmd.Process == nil { 653 return 654 } 655 SignalDaemonDump(d.cmd.Process.Pid) 656 } 657 658 // Stop will send a SIGINT every second and wait for the daemon to stop. 659 // If it times out, a SIGKILL is sent. 660 // Stop will not delete the daemon directory. If a purged daemon is needed, 661 // instantiate a new one with NewDaemon. 662 // If an error occurs while starting the daemon, the test will fail. 663 func (d *Daemon) Stop(t testing.TB) { 664 t.Helper() 665 err := d.StopWithError() 666 if err != nil { 667 if err != errDaemonNotStarted { 668 t.Fatalf("[%s] error while stopping the daemon: %v", d.id, err) 669 } else { 670 t.Logf("[%s] daemon is not started", d.id) 671 } 672 } 673 } 674 675 // StopWithError will send a SIGINT every second and wait for the daemon to stop. 676 // If it timeouts, a SIGKILL is sent. 677 // Stop will not delete the daemon directory. If a purged daemon is needed, 678 // instantiate a new one with NewDaemon. 679 func (d *Daemon) StopWithError() (err error) { 680 if d.cmd == nil || d.Wait == nil { 681 return errDaemonNotStarted 682 } 683 defer func() { 684 if err != nil { 685 d.log.Logf("[%s] error while stopping daemon: %v", d.id, err) 686 } else { 687 d.log.Logf("[%s] daemon stopped", d.id) 688 if d.pidFile != "" { 689 _ = os.Remove(d.pidFile) 690 } 691 } 692 if err := d.logFile.Close(); err != nil { 693 d.log.Logf("[%s] failed to close daemon logfile: %v", d.id, err) 694 } 695 d.cmd = nil 696 }() 697 698 i := 1 699 ticker := time.NewTicker(time.Second) 700 defer ticker.Stop() 701 tick := ticker.C 702 703 d.log.Logf("[%s] stopping daemon", d.id) 704 705 if err := d.cmd.Process.Signal(os.Interrupt); err != nil { 706 if strings.Contains(err.Error(), "os: process already finished") { 707 return errDaemonNotStarted 708 } 709 return errors.Wrapf(err, "[%s] could not send signal", d.id) 710 } 711 712 out1: 713 for { 714 select { 715 case err := <-d.Wait: 716 return err 717 case <-time.After(20 * time.Second): 718 // time for stopping jobs and run onShutdown hooks 719 d.log.Logf("[%s] daemon stop timed out after 20 seconds", d.id) 720 break out1 721 } 722 } 723 724 out2: 725 for { 726 select { 727 case err := <-d.Wait: 728 return err 729 case <-tick: 730 i++ 731 if i > 5 { 732 d.log.Logf("[%s] tried to interrupt daemon for %d times, now try to kill it", d.id, i) 733 break out2 734 } 735 d.log.Logf("[%d] attempt #%d/5: daemon is still running with pid %d", i, d.cmd.Process.Pid) 736 if err := d.cmd.Process.Signal(os.Interrupt); err != nil { 737 return errors.Wrapf(err, "[%s] attempt #%d/5 could not send signal", d.id, i) 738 } 739 } 740 } 741 742 if err := d.cmd.Process.Kill(); err != nil { 743 d.log.Logf("[%s] failed to kill daemon: %v", d.id, err) 744 return err 745 } 746 747 return nil 748 } 749 750 // Restart will restart the daemon by first stopping it and the starting it. 751 // If an error occurs while starting the daemon, the test will fail. 752 func (d *Daemon) Restart(t testing.TB, args ...string) { 753 t.Helper() 754 d.Stop(t) 755 d.Start(t, args...) 756 } 757 758 // RestartWithError will restart the daemon by first stopping it and then starting it. 759 func (d *Daemon) RestartWithError(arg ...string) error { 760 if err := d.StopWithError(); err != nil { 761 return err 762 } 763 return d.StartWithError(arg...) 764 } 765 766 func (d *Daemon) handleUserns() { 767 // in the case of tests running a user namespace-enabled daemon, we have resolved 768 // d.Root to be the actual final path of the graph dir after the "uid.gid" of 769 // remapped root is added--we need to subtract it from the path before calling 770 // start or else we will continue making subdirectories rather than truly restarting 771 // with the same location/root: 772 if root := os.Getenv("DOCKER_REMAP_ROOT"); root != "" { 773 d.Root = filepath.Dir(d.Root) 774 } 775 } 776 777 // ReloadConfig asks the daemon to reload its configuration 778 func (d *Daemon) ReloadConfig() error { 779 if d.cmd == nil || d.cmd.Process == nil { 780 return errors.New("daemon is not running") 781 } 782 783 errCh := make(chan error, 1) 784 started := make(chan struct{}) 785 go func() { 786 _, body, err := request.Get(context.TODO(), "/events", request.Host(d.Sock())) 787 close(started) 788 if err != nil { 789 errCh <- err 790 return 791 } 792 defer body.Close() 793 dec := json.NewDecoder(body) 794 for { 795 var e events.Message 796 if err := dec.Decode(&e); err != nil { 797 errCh <- err 798 return 799 } 800 if e.Type != events.DaemonEventType { 801 continue 802 } 803 if e.Action != events.ActionReload { 804 continue 805 } 806 close(errCh) // notify that we are done 807 return 808 } 809 }() 810 811 <-started 812 if err := signalDaemonReload(d.cmd.Process.Pid); err != nil { 813 return errors.Wrapf(err, "[%s] error signaling daemon reload", d.id) 814 } 815 select { 816 case err := <-errCh: 817 if err != nil { 818 return errors.Wrapf(err, "[%s] error waiting for daemon reload event", d.id) 819 } 820 case <-time.After(30 * time.Second): 821 return errors.Errorf("[%s] daemon reload event timed out after 30 seconds", d.id) 822 } 823 return nil 824 } 825 826 // LoadBusybox image into the daemon 827 func (d *Daemon) LoadBusybox(ctx context.Context, t testing.TB) { 828 t.Helper() 829 clientHost, err := client.NewClientWithOpts(client.FromEnv) 830 assert.NilError(t, err, "[%s] failed to create client", d.id) 831 defer clientHost.Close() 832 833 reader, err := clientHost.ImageSave(ctx, []string{"busybox:latest"}) 834 assert.NilError(t, err, "[%s] failed to download busybox", d.id) 835 defer reader.Close() 836 837 c := d.NewClientT(t) 838 defer c.Close() 839 840 resp, err := c.ImageLoad(ctx, reader, true) 841 assert.NilError(t, err, "[%s] failed to load busybox", d.id) 842 defer resp.Body.Close() 843 } 844 845 func (d *Daemon) getClientConfig() (*clientConfig, error) { 846 var ( 847 transport *http.Transport 848 scheme string 849 addr string 850 proto string 851 ) 852 if d.UseDefaultTLSHost { 853 option := &tlsconfig.Options{ 854 CAFile: "fixtures/https/ca.pem", 855 CertFile: "fixtures/https/client-cert.pem", 856 KeyFile: "fixtures/https/client-key.pem", 857 } 858 tlsConfig, err := tlsconfig.Client(*option) 859 if err != nil { 860 return nil, err 861 } 862 transport = &http.Transport{ 863 TLSClientConfig: tlsConfig, 864 } 865 addr = defaultTLSHost 866 scheme = "https" 867 proto = "tcp" 868 } else if d.UseDefaultHost { 869 addr = defaultUnixSocket 870 proto = "unix" 871 scheme = "http" 872 transport = &http.Transport{} 873 } else { 874 addr = d.sockPath() 875 proto = "unix" 876 scheme = "http" 877 transport = &http.Transport{} 878 } 879 880 if err := sockets.ConfigureTransport(transport, proto, addr); err != nil { 881 return nil, err 882 } 883 transport.DisableKeepAlives = true 884 if proto == "unix" { 885 addr = filepath.Base(addr) 886 } 887 return &clientConfig{ 888 transport: transport, 889 scheme: scheme, 890 addr: addr, 891 }, nil 892 } 893 894 func (d *Daemon) queryRootDir() (string, error) { 895 // update daemon root by asking /info endpoint (to support user 896 // namespaced daemon with root remapped uid.gid directory) 897 clientConfig, err := d.getClientConfig() 898 if err != nil { 899 return "", err 900 } 901 902 c := &http.Client{ 903 Transport: clientConfig.transport, 904 } 905 906 req, err := http.NewRequest(http.MethodGet, "/info", nil) 907 if err != nil { 908 return "", err 909 } 910 req.Header.Set("Content-Type", "application/json") 911 req.URL.Host = clientConfig.addr 912 req.URL.Scheme = clientConfig.scheme 913 914 resp, err := c.Do(req) 915 if err != nil { 916 return "", err 917 } 918 body := ioutils.NewReadCloserWrapper(resp.Body, func() error { 919 return resp.Body.Close() 920 }) 921 922 type Info struct { 923 DockerRootDir string 924 } 925 var b []byte 926 var i Info 927 b, err = request.ReadBody(body) 928 if err == nil && resp.StatusCode == http.StatusOK { 929 // read the docker root dir 930 if err = json.Unmarshal(b, &i); err == nil { 931 return i.DockerRootDir, nil 932 } 933 } 934 return "", err 935 } 936 937 // Info returns the info struct for this daemon 938 func (d *Daemon) Info(t testing.TB) system.Info { 939 t.Helper() 940 c := d.NewClientT(t) 941 info, err := c.Info(context.Background()) 942 assert.NilError(t, err) 943 assert.NilError(t, c.Close()) 944 return info 945 } 946 947 // TamperWithContainerConfig modifies the on-disk config of a container. 948 func (d *Daemon) TamperWithContainerConfig(t testing.TB, containerID string, tamper func(*container.Container)) { 949 t.Helper() 950 951 configPath := filepath.Join(d.Root, "containers", containerID, "config.v2.json") 952 configBytes, err := os.ReadFile(configPath) 953 assert.NilError(t, err) 954 955 var c container.Container 956 assert.NilError(t, json.Unmarshal(configBytes, &c)) 957 c.State = container.NewState() 958 tamper(&c) 959 configBytes, err = json.Marshal(&c) 960 assert.NilError(t, err) 961 assert.NilError(t, os.WriteFile(configPath, configBytes, 0o600)) 962 } 963 964 // cleanupRaftDir removes swarmkit wal files if present 965 func cleanupRaftDir(t testing.TB, d *Daemon) { 966 t.Helper() 967 for _, p := range []string{"wal", "wal-v3-encrypted", "snap-v3-encrypted"} { 968 dir := filepath.Join(d.Root, "swarm/raft", p) 969 if err := os.RemoveAll(dir); err != nil { 970 t.Logf("[%s] error removing %v: %v", d.id, dir, err) 971 } 972 } 973 } 974 975 // cleanupDaemonStorage removes the daemon's storage directory. 976 // 977 // Note that we don't delete the whole directory, as some files (e.g. daemon 978 // logs) are collected for inclusion in the "bundles" that are stored as Jenkins 979 // artifacts. 980 // 981 // We currently do not include container logs in the bundles, so this also 982 // removes the "containers" sub-directory. 983 func cleanupDaemonStorage(t testing.TB, d *Daemon) { 984 t.Helper() 985 dirs := []string{ 986 "builder", 987 "buildkit", 988 "containers", 989 "image", 990 "network", 991 "plugins", 992 "tmp", 993 "trust", 994 "volumes", 995 // note: this assumes storage-driver name matches the subdirectory, 996 // which is currently true, but not guaranteed. 997 d.storageDriver, 998 } 999 1000 for _, p := range dirs { 1001 dir := filepath.Join(d.Root, p) 1002 if err := os.RemoveAll(dir); err != nil { 1003 t.Logf("[%s] error removing %v: %v", d.id, dir, err) 1004 } 1005 } 1006 }