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