package testcontainers import ( "archive/tar" "bufio" "bytes" "context" "encoding/base64" "encoding/binary" "encoding/json" "errors" "fmt" "io" "net/url" "os" "path/filepath" "strings" "sync" "time" "github.com/cenkalti/backoff/v4" "github.com/containerd/containerd/platforms" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-connections/nat" "github.com/google/uuid" "github.com/moby/term" specs "github.com/opencontainers/image-spec/specs-go/v1" tcexec "github.com/testcontainers/testcontainers-go/exec" "github.com/testcontainers/testcontainers-go/internal" "github.com/testcontainers/testcontainers-go/internal/testcontainersdocker" "github.com/testcontainers/testcontainers-go/internal/testcontainerssession" "github.com/testcontainers/testcontainers-go/wait" ) var ( // Implement interfaces _ Container = (*DockerContainer)(nil) logOnce sync.Once ErrDuplicateMountTarget = errors.New("duplicate mount target detected") ) const ( Bridge = "bridge" // Bridge network name (as well as driver) Podman = "podman" ReaperDefault = "reaper_default" // Default network name when bridge is not available packagePath = "github.com/testcontainers/testcontainers-go" logStoppedForOutOfSyncMessage = "Stopping log consumer: Headers out of sync" ) // DockerContainer represents a container started using Docker type DockerContainer struct { // Container ID from Docker ID string WaitingFor wait.Strategy Image string isRunning bool imageWasBuilt bool provider *DockerProvider sessionID uuid.UUID terminationSignal chan bool consumers []LogConsumer raw *types.ContainerJSON stopProducer chan bool logger Logging lifecycleHooks []ContainerLifecycleHooks } // SetLogger sets the logger for the container func (c *DockerContainer) SetLogger(logger Logging) { c.logger = logger } // SetProvider sets the provider for the container func (c *DockerContainer) SetProvider(provider *DockerProvider) { c.provider = provider } func (c *DockerContainer) GetContainerID() string { return c.ID } func (c *DockerContainer) IsRunning() bool { return c.isRunning } // Endpoint gets proto://host:port string for the first exposed port // Will returns just host:port if proto is "" func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, error) { ports, err := c.Ports(ctx) if err != nil { return "", err } // get first port var firstPort nat.Port for p := range ports { firstPort = p break } return c.PortEndpoint(ctx, firstPort, proto) } // PortEndpoint gets proto://host:port string for the given exposed port // Will returns just host:port if proto is "" func (c *DockerContainer) PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) { host, err := c.Host(ctx) if err != nil { return "", err } outerPort, err := c.MappedPort(ctx, port) if err != nil { return "", err } protoFull := "" if proto != "" { protoFull = fmt.Sprintf("%s://", proto) } return fmt.Sprintf("%s%s:%s", protoFull, host, outerPort.Port()), nil } // Host gets host (ip or name) of the docker daemon where the container port is exposed // Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel // You can use the "TC_HOST" env variable to set this yourself func (c *DockerContainer) Host(ctx context.Context) (string, error) { host, err := c.provider.DaemonHost(ctx) if err != nil { return "", err } return host, nil } // MappedPort gets externally mapped port for a container port func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) { inspect, err := c.inspectContainer(ctx) if err != nil { return "", err } if inspect.ContainerJSONBase.HostConfig.NetworkMode == "host" { return port, nil } ports, err := c.Ports(ctx) if err != nil { return "", err } for k, p := range ports { if k.Port() != port.Port() { continue } if port.Proto() != "" && k.Proto() != port.Proto() { continue } if len(p) == 0 { continue } return nat.NewPort(k.Proto(), p[0].HostPort) } return "", errors.New("port not found") } // Ports gets the exposed ports for the container. func (c *DockerContainer) Ports(ctx context.Context) (nat.PortMap, error) { inspect, err := c.inspectContainer(ctx) if err != nil { return nil, err } return inspect.NetworkSettings.Ports, nil } // SessionID gets the current session id func (c *DockerContainer) SessionID() string { return c.sessionID.String() } // Start will start an already created container func (c *DockerContainer) Start(ctx context.Context) error { err := c.startingHook(ctx) if err != nil { return err } if err := c.provider.client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil { return err } defer c.provider.Close() err = c.startedHook(ctx) if err != nil { return err } return nil } // Stop will stop an already started container // // In case the container fails to stop // gracefully within a time frame specified by the timeout argument, // it is forcefully terminated (killed). // // If the timeout is nil, the container's StopTimeout value is used, if set, // otherwise the engine default. A negative timeout value can be specified, // meaning no timeout, i.e. no forceful termination is performed. func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) error { err := c.stoppingHook(ctx) if err != nil { return err } var options container.StopOptions if timeout != nil { timeoutSeconds := int(timeout.Seconds()) options.Timeout = &timeoutSeconds } if err := c.provider.client.ContainerStop(ctx, c.ID, options); err != nil { return err } defer c.provider.Close() c.isRunning = false err = c.stoppedHook(ctx) if err != nil { return err } return nil } // Terminate is used to kill the container. It is usually triggered by as defer function. func (c *DockerContainer) Terminate(ctx context.Context) error { select { // close reaper if it was created case c.terminationSignal <- true: default: } defer c.provider.client.Close() err := c.terminatingHook(ctx) if err != nil { return err } err = c.StopLogProducer() if err != nil { return err } err = c.provider.client.ContainerRemove(ctx, c.GetContainerID(), types.ContainerRemoveOptions{ RemoveVolumes: true, Force: true, }) if err != nil { return err } err = c.terminatedHook(ctx) if err != nil { return err } if c.imageWasBuilt { _, err := c.provider.client.ImageRemove(ctx, c.Image, types.ImageRemoveOptions{ Force: true, PruneChildren: true, }) if err != nil { return err } } c.sessionID = uuid.UUID{} c.isRunning = false return nil } // update container raw info func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*types.ContainerJSON, error) { defer c.provider.Close() inspect, err := c.provider.client.ContainerInspect(ctx, c.ID) if err != nil { return nil, err } c.raw = &inspect return c.raw, nil } func (c *DockerContainer) inspectContainer(ctx context.Context) (*types.ContainerJSON, error) { defer c.provider.Close() inspect, err := c.provider.client.ContainerInspect(ctx, c.ID) if err != nil { return nil, err } return &inspect, nil } // Logs will fetch both STDOUT and STDERR from the current container. Returns a // ReadCloser and leaves it up to the caller to extract what it wants. func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) { const streamHeaderSize = 8 options := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, } rc, err := c.provider.client.ContainerLogs(ctx, c.ID, options) if err != nil { return nil, err } defer c.provider.Close() pr, pw := io.Pipe() r := bufio.NewReader(rc) go func() { var lineStarted = true for err == nil { line, isPrefix, err := r.ReadLine() if lineStarted && len(line) >= streamHeaderSize { line = line[streamHeaderSize:] // trim stream header lineStarted = false } if !isPrefix { lineStarted = true } _, errW := pw.Write(line) if errW != nil { return } if !isPrefix { _, errW := pw.Write([]byte("\n")) if errW != nil { return } } if err != nil { _ = pw.CloseWithError(err) return } } }() return pr, nil } // FollowOutput adds a LogConsumer to be sent logs from the container's // STDOUT and STDERR func (c *DockerContainer) FollowOutput(consumer LogConsumer) { c.consumers = append(c.consumers, consumer) } // Name gets the name of the container. func (c *DockerContainer) Name(ctx context.Context) (string, error) { inspect, err := c.inspectContainer(ctx) if err != nil { return "", err } return inspect.Name, nil } // State returns container's running state func (c *DockerContainer) State(ctx context.Context) (*types.ContainerState, error) { inspect, err := c.inspectRawContainer(ctx) if err != nil { if c.raw != nil { return c.raw.State, err } return nil, err } return inspect.State, nil } // Networks gets the names of the networks the container is attached to. func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) { inspect, err := c.inspectContainer(ctx) if err != nil { return []string{}, err } networks := inspect.NetworkSettings.Networks n := []string{} for k := range networks { n = append(n, k) } return n, nil } // ContainerIP gets the IP address of the primary network within the container. func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) { inspect, err := c.inspectContainer(ctx) if err != nil { return "", err } ip := inspect.NetworkSettings.IPAddress if ip == "" { // use IP from "Networks" if only single network defined networks := inspect.NetworkSettings.Networks if len(networks) == 1 { for _, v := range networks { ip = v.IPAddress } } } return ip, nil } // ContainerIPs gets the IP addresses of all the networks within the container. func (c *DockerContainer) ContainerIPs(ctx context.Context) ([]string, error) { ips := make([]string, 0) inspect, err := c.inspectContainer(ctx) if err != nil { return nil, err } networks := inspect.NetworkSettings.Networks for _, nw := range networks { ips = append(ips, nw.IPAddress) } return ips, nil } // NetworkAliases gets the aliases of the container for the networks it is attached to. func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]string, error) { inspect, err := c.inspectContainer(ctx) if err != nil { return map[string][]string{}, err } networks := inspect.NetworkSettings.Networks a := map[string][]string{} for k := range networks { a[k] = networks[k].Aliases } return a, nil } func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) { cli := c.provider.client response, err := cli.ContainerExecCreate(ctx, c.ID, types.ExecConfig{ Cmd: cmd, Detach: false, AttachStdout: true, AttachStderr: true, }) if err != nil { return 0, nil, err } hijack, err := cli.ContainerExecAttach(ctx, response.ID, types.ExecStartCheck{}) if err != nil { return 0, nil, err } opt := &tcexec.ProcessOptions{ Reader: hijack.Reader, } for _, o := range options { o.Apply(opt) } var exitCode int for { execResp, err := cli.ContainerExecInspect(ctx, response.ID) if err != nil { return 0, nil, err } if !execResp.Running { exitCode = execResp.ExitCode break } time.Sleep(100 * time.Millisecond) } return exitCode, opt.Reader, nil } type FileFromContainer struct { underlying *io.ReadCloser tarreader *tar.Reader } func (fc *FileFromContainer) Read(b []byte) (int, error) { return (*fc.tarreader).Read(b) } func (fc *FileFromContainer) Close() error { return (*fc.underlying).Close() } func (c *DockerContainer) CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) { r, _, err := c.provider.client.CopyFromContainer(ctx, c.ID, filePath) if err != nil { return nil, err } defer c.provider.Close() tarReader := tar.NewReader(r) // if we got here we have exactly one file in the TAR-stream // so we advance the index by one so the next call to Read will start reading it _, err = tarReader.Next() if err != nil { return nil, err } ret := &FileFromContainer{ underlying: &r, tarreader: tarReader, } return ret, nil } // CopyDirToContainer copies the contents of a directory to a parent path in the container. This parent path must exist in the container first // as we cannot create it func (c *DockerContainer) CopyDirToContainer(ctx context.Context, hostDirPath string, containerParentPath string, fileMode int64) error { dir, err := isDir(hostDirPath) if err != nil { return err } if !dir { // it's not a dir: let the consumer to handle an error return fmt.Errorf("path %s is not a directory", hostDirPath) } buff, err := tarDir(hostDirPath, fileMode) if err != nil { return err } // create the directory under its parent parent := filepath.Dir(containerParentPath) err = c.provider.client.CopyToContainer(ctx, c.ID, parent, buff, types.CopyToContainerOptions{}) if err != nil { return err } defer c.provider.Close() return nil } func (c *DockerContainer) CopyFileToContainer(ctx context.Context, hostFilePath string, containerFilePath string, fileMode int64) error { dir, err := isDir(hostFilePath) if err != nil { return err } if dir { return c.CopyDirToContainer(ctx, hostFilePath, containerFilePath, fileMode) } fileContent, err := os.ReadFile(hostFilePath) if err != nil { return err } return c.CopyToContainer(ctx, fileContent, containerFilePath, fileMode) } // CopyToContainer copies fileContent data to a file in container func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error { buffer, err := tarFile(fileContent, containerFilePath, fileMode) if err != nil { return err } err = c.provider.client.CopyToContainer(ctx, c.ID, filepath.Dir(containerFilePath), buffer, types.CopyToContainerOptions{}) if err != nil { return err } defer c.provider.Close() return nil } // StartLogProducer will start a concurrent process that will continuously read logs // from the container and will send them to each added LogConsumer func (c *DockerContainer) StartLogProducer(ctx context.Context) error { if c.stopProducer != nil { return errors.New("log producer already started") } c.stopProducer = make(chan bool) go func(stop <-chan bool) { since := "" // if the socket is closed we will make additional logs request with updated Since timestamp BEGIN: options := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Follow: true, Since: since, } ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() r, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options) if err != nil { // if we can't get the logs, panic, we can't return an error to anything // from within this goroutine panic(err) } defer c.provider.Close() for { select { case <-stop: err := r.Close() if err != nil { // we can't close the read closer, this should never happen panic(err) } return default: h := make([]byte, 8) _, err := io.ReadFull(r, h) if err != nil { // proper type matching requires https://go-review.googlesource.com/c/go/+/250357/ (go 1.16) if strings.Contains(err.Error(), "use of closed network connection") { now := time.Now() since = fmt.Sprintf("%d.%09d", now.Unix(), int64(now.Nanosecond())) goto BEGIN } if errors.Is(err, context.DeadlineExceeded) { // Probably safe to continue here continue } _, _ = fmt.Fprintf(os.Stderr, "container log error: %+v. %s", err, logStoppedForOutOfSyncMessage) // if we would continue here, the next header-read will result into random data... return } count := binary.BigEndian.Uint32(h[4:]) if count == 0 { continue } logType := h[0] if logType > 2 { _, _ = fmt.Fprintf(os.Stderr, "received invalid log type: %d", logType) // sometimes docker returns logType = 3 which is an undocumented log type, so treat it as stdout logType = 1 } // a map of the log type --> int representation in the header, notice the first is blank, this is stdin, but the go docker client doesn't allow following that in logs logTypes := []string{"", StdoutLog, StderrLog} b := make([]byte, count) _, err = io.ReadFull(r, b) if err != nil { // TODO: add-logger: use logger to log out this error _, _ = fmt.Fprintf(os.Stderr, "error occurred reading log with known length %s", err.Error()) if errors.Is(err, context.DeadlineExceeded) { // Probably safe to continue here continue } // we can not continue here as the next read most likely will not be the next header _, _ = fmt.Fprintln(os.Stderr, logStoppedForOutOfSyncMessage) return } for _, c := range c.consumers { c.Accept(Log{ LogType: logTypes[logType], Content: b, }) } } } }(c.stopProducer) return nil } // StopLogProducer will stop the concurrent process that is reading logs // and sending them to each added LogConsumer func (c *DockerContainer) StopLogProducer() error { if c.stopProducer != nil { c.stopProducer <- true c.stopProducer = nil } return nil } // DockerNetwork represents a network started using Docker type DockerNetwork struct { ID string // Network ID from Docker Driver string Name string provider *DockerProvider terminationSignal chan bool } // Remove is used to remove the network. It is usually triggered by as defer function. func (n *DockerNetwork) Remove(ctx context.Context) error { select { // close reaper if it was created case n.terminationSignal <- true: default: } defer n.provider.Close() return n.provider.client.NetworkRemove(ctx, n.ID) } // DockerProvider implements the ContainerProvider interface type DockerProvider struct { *DockerProviderOptions client client.APIClient host string hostCache string config TestcontainersConfig } // Client gets the docker client used by the provider func (p *DockerProvider) Client() client.APIClient { return p.client } // Close closes the docker client used by the provider func (p *DockerProvider) Close() error { if p.client == nil { return nil } return p.client.Close() } // SetClient sets the docker client to be used by the provider func (p *DockerProvider) SetClient(c client.APIClient) { p.client = c } var _ ContainerProvider = (*DockerProvider)(nil) func NewDockerClient() (cli *client.Client, err error) { return testcontainersdocker.NewClient(context.Background()) } // BuildImage will build and image from context and Dockerfile, then return the tag func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (string, error) { repo := uuid.New() tag := uuid.New() repoTag := fmt.Sprintf("%s:%s", repo, tag) buildContext, err := img.GetContext() if err != nil { return "", err } buildOptions := types.ImageBuildOptions{ BuildArgs: img.GetBuildArgs(), Dockerfile: img.GetDockerfile(), AuthConfigs: img.GetAuthConfigs(), Context: buildContext, Tags: []string{repoTag}, Remove: true, ForceRemove: true, } var resp types.ImageBuildResponse err = backoff.Retry(func() error { resp, err = p.client.ImageBuild(ctx, buildContext, buildOptions) if err != nil { if _, ok := err.(errdefs.ErrNotFound); ok { return backoff.Permanent(err) } Logger.Printf("Failed to build image: %s, will retry", err) return err } defer p.Close() return nil }, backoff.WithContext(backoff.NewExponentialBackOff(), ctx)) if err != nil { return "", err } if img.ShouldPrintBuildLog() { termFd, isTerm := term.GetFdInfo(os.Stderr) err = jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stderr, termFd, isTerm, nil) if err != nil { return "", err } } // need to read the response from Docker, I think otherwise the image // might not finish building before continuing to execute here buf := new(bytes.Buffer) _, err = buf.ReadFrom(resp.Body) if err != nil { return "", err } _ = resp.Body.Close() return repoTag, nil } // CreateContainer fulfills a request for a container without starting it func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { var err error // defer the close of the Docker client connection the soonest defer p.Close() // Make sure that bridge network exists // In case it is disabled we will create reaper_default network if p.DefaultNetwork == "" { p.DefaultNetwork, err = p.getDefaultNetwork(ctx, p.client) if err != nil { return nil, err } } // If default network is not bridge make sure it is attached to the request // as container won't be attached to it automatically // in case of Podman the bridge network is called 'podman' as 'bridge' would conflict if p.DefaultNetwork != p.defaultBridgeNetworkName { isAttached := false for _, net := range req.Networks { if net == p.DefaultNetwork { isAttached = true break } } if !isAttached { req.Networks = append(req.Networks, p.DefaultNetwork) } } env := []string{} for envKey, envVar := range req.Env { env = append(env, envKey+"="+envVar) } if req.Labels == nil { req.Labels = make(map[string]string) } reaperOpts := containerOptions{ ImageName: req.ReaperImage, } for _, opt := range req.ReaperOptions { opt(&reaperOpts) } tcConfig := p.Config().Config var termSignal chan bool // the reaper does not need to start a reaper for itself isReaperContainer := strings.EqualFold(req.Image, reaperImage(reaperOpts.ImageName)) if !tcConfig.RyukDisabled && !isReaperContainer { r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating reaper failed", err) } termSignal, err = r.Connect() if err != nil { return nil, fmt.Errorf("%w: connecting to reaper failed", err) } for k, v := range r.Labels() { if _, ok := req.Labels[k]; !ok { req.Labels[k] = v } } } // Cleanup on error, otherwise set termSignal to nil before successful return. defer func() { if termSignal != nil { termSignal <- true } }() if err = req.Validate(); err != nil { return nil, err } var tag string var platform *specs.Platform if req.ShouldBuildImage() { tag, err = p.BuildImage(ctx, &req) if err != nil { return nil, err } } else { tag = req.Image if req.ImagePlatform != "" { p, err := platforms.Parse(req.ImagePlatform) if err != nil { return nil, fmt.Errorf("invalid platform %s: %w", req.ImagePlatform, err) } platform = &p } var shouldPullImage bool if req.AlwaysPullImage { shouldPullImage = true // If requested always attempt to pull image } else { image, _, err := p.client.ImageInspectWithRaw(ctx, tag) if err != nil { if client.IsErrNotFound(err) { shouldPullImage = true } else { return nil, err } } if platform != nil && (image.Architecture != platform.Architecture || image.Os != platform.OS) { shouldPullImage = true } } if shouldPullImage { pullOpt := types.ImagePullOptions{ Platform: req.ImagePlatform, // may be empty } registry, imageAuth, err := DockerImageAuth(ctx, req.Image) if err != nil { p.Logger.Printf("Failed to get image auth for %s. Setting empty credentials for the image: %s. Error is:%s", registry, req.Image, err) } else { // see https://github.com/docker/docs/blob/e8e1204f914767128814dca0ea008644709c117f/engine/api/sdk/examples.md?plain=1#L649-L657 encodedJSON, err := json.Marshal(imageAuth) if err != nil { p.Logger.Printf("Failed to marshal image auth. Setting empty credentials for the image: %s. Error is:%s", req.Image, err) } else { pullOpt.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON) } } if err := p.attemptToPullImage(ctx, tag, pullOpt); err != nil { return nil, err } } } for k, v := range testcontainersdocker.DefaultLabels() { req.Labels[k] = v } dockerInput := &container.Config{ Entrypoint: req.Entrypoint, Image: tag, Env: env, Labels: req.Labels, Cmd: req.Cmd, Hostname: req.Hostname, User: req.User, } hostConfig := &container.HostConfig{ Privileged: req.Privileged, ShmSize: req.ShmSize, Tmpfs: req.Tmpfs, } networkingConfig := &network.NetworkingConfig{} // default hooks include logger hook and pre-create hook defaultHooks := []ContainerLifecycleHooks{ DefaultLoggingHook(p.Logger), { PreCreates: []ContainerRequestHook{ func(ctx context.Context, req ContainerRequest) error { return p.preCreateContainerHook(ctx, req, dockerInput, hostConfig, networkingConfig) }, }, PostCreates: []ContainerHook{ // copy files to container after it's created func(ctx context.Context, c Container) error { for _, f := range req.Files { err := c.CopyFileToContainer(ctx, f.HostFilePath, f.ContainerFilePath, f.FileMode) if err != nil { return fmt.Errorf("can't copy %s to container: %w", f.HostFilePath, err) } } return nil }, }, PostStarts: []ContainerHook{ // first post-start hook is to wait for the container to be ready func(ctx context.Context, c Container) error { dockerContainer := c.(*DockerContainer) // if a Wait Strategy has been specified, wait before returning if dockerContainer.WaitingFor != nil { dockerContainer.logger.Printf( "🚧 Waiting for container id %s image: %s. Waiting for: %+v", dockerContainer.ID[:12], dockerContainer.Image, dockerContainer.WaitingFor, ) if err := dockerContainer.WaitingFor.WaitUntilReady(ctx, c); err != nil { return err } } dockerContainer.isRunning = true return nil }, }, }, } // always prepend default lifecycle hooks to user-defined hooks req.LifecycleHooks = append(defaultHooks, req.LifecycleHooks...) err = req.creatingHook(ctx) if err != nil { return nil, err } resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, networkingConfig, platform, req.Name) if err != nil { return nil, err } // #248: If there is more than one network specified in the request attach newly created container to them one by one if len(req.Networks) > 1 { for _, n := range req.Networks[1:] { nw, err := p.GetNetwork(ctx, NetworkRequest{ Name: n, }) if err == nil { endpointSetting := network.EndpointSettings{ Aliases: req.NetworkAliases[n], } err = p.client.NetworkConnect(ctx, nw.ID, resp.ID, &endpointSetting) if err != nil { return nil, err } } } } c := &DockerContainer{ ID: resp.ID, WaitingFor: req.WaitingFor, Image: tag, imageWasBuilt: req.ShouldBuildImage(), sessionID: testcontainerssession.ID(), provider: p, terminationSignal: termSignal, stopProducer: nil, logger: p.Logger, lifecycleHooks: req.LifecycleHooks, } err = c.createdHook(ctx) if err != nil { return nil, err } // Disable cleanup on success termSignal = nil return c, nil } func (p *DockerProvider) findContainerByName(ctx context.Context, name string) (*types.Container, error) { if name == "" { return nil, nil } // Note that, 'name' filter will use regex to find the containers filter := filters.NewArgs(filters.Arg("name", fmt.Sprintf("^%s$", name))) containers, err := p.client.ContainerList(ctx, types.ContainerListOptions{Filters: filter}) if err != nil { return nil, err } defer p.Close() if len(containers) > 0 { return &containers[0], nil } return nil, nil } func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { c, err := p.findContainerByName(ctx, req.Name) if err != nil { return nil, err } if c == nil { return p.CreateContainer(ctx, req) } tcConfig := p.Config().Config var termSignal chan bool if !tcConfig.RyukDisabled { r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating reaper failed", err) } termSignal, err = r.Connect() if err != nil { return nil, fmt.Errorf("%w: connecting to reaper failed", err) } } dc := &DockerContainer{ ID: c.ID, WaitingFor: req.WaitingFor, Image: c.Image, sessionID: testcontainerssession.ID(), provider: p, terminationSignal: termSignal, stopProducer: nil, logger: p.Logger, isRunning: c.State == "running", } return dc, nil } // attemptToPullImage tries to pull the image while respecting the ctx cancellations. // Besides, if the image cannot be pulled due to ErrorNotFound then no need to retry but terminate immediately. func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pullOpt types.ImagePullOptions) error { var ( err error pull io.ReadCloser ) err = backoff.Retry(func() error { pull, err = p.client.ImagePull(ctx, tag, pullOpt) if err != nil { if _, ok := err.(errdefs.ErrNotFound); ok { return backoff.Permanent(err) } Logger.Printf("Failed to pull image: %s, will retry", err) return err } defer p.Close() return nil }, backoff.WithContext(backoff.NewExponentialBackOff(), ctx)) if err != nil { return err } defer pull.Close() // download of docker image finishes at EOF of the pull request _, err = io.ReadAll(pull) return err } // Health measure the healthiness of the provider. Right now we leverage the // docker-client ping endpoint to see if the daemon is reachable. func (p *DockerProvider) Health(ctx context.Context) (err error) { _, err = p.client.Ping(ctx) defer p.Close() return err } // RunContainer takes a RequestContainer as input and it runs a container via the docker sdk func (p *DockerProvider) RunContainer(ctx context.Context, req ContainerRequest) (Container, error) { c, err := p.CreateContainer(ctx, req) if err != nil { return nil, err } if err := c.Start(ctx); err != nil { return c, fmt.Errorf("%w: could not start container", err) } return c, nil } // Config provides the TestcontainersConfig read from $HOME/.testcontainers.properties or // the environment variables func (p *DockerProvider) Config() TestcontainersConfig { return p.config } // DaemonHost gets the host or ip of the Docker daemon where ports are exposed on // Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel // You can use the "TC_HOST" env variable to set this yourself func (p *DockerProvider) DaemonHost(ctx context.Context) (string, error) { return daemonHost(ctx, p) } func daemonHost(ctx context.Context, p *DockerProvider) (string, error) { if p.hostCache != "" { return p.hostCache, nil } host, exists := os.LookupEnv("TC_HOST") if exists { p.hostCache = host return p.hostCache, nil } // infer from Docker host url, err := url.Parse(p.client.DaemonHost()) if err != nil { return "", err } defer p.Close() switch url.Scheme { case "http", "https", "tcp": p.hostCache = url.Hostname() case "unix", "npipe": if testcontainersdocker.InAContainer() { ip, err := p.GetGatewayIP(ctx) if err != nil { ip, err = testcontainersdocker.DefaultGatewayIP() if err != nil { ip = "localhost" } } p.hostCache = ip } else { p.hostCache = "localhost" } default: return "", errors.New("could not determine host through env or docker host") } return p.hostCache, nil } // CreateNetwork returns the object representing a new network identified by its name func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (Network, error) { var err error // defer the close of the Docker client connection the soonest defer p.Close() // Make sure that bridge network exists // In case it is disabled we will create reaper_default network if p.DefaultNetwork == "" { if p.DefaultNetwork, err = p.getDefaultNetwork(ctx, p.client); err != nil { return nil, err } } if req.Labels == nil { req.Labels = make(map[string]string) } tcConfig := p.Config().Config nc := types.NetworkCreate{ Driver: req.Driver, CheckDuplicate: req.CheckDuplicate, Internal: req.Internal, EnableIPv6: req.EnableIPv6, Attachable: req.Attachable, Labels: req.Labels, IPAM: req.IPAM, } var termSignal chan bool if !tcConfig.RyukDisabled { r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...) if err != nil { return nil, fmt.Errorf("%w: creating network reaper failed", err) } termSignal, err = r.Connect() if err != nil { return nil, fmt.Errorf("%w: connecting to network reaper failed", err) } for k, v := range r.Labels() { if _, ok := req.Labels[k]; !ok { req.Labels[k] = v } } } // Cleanup on error, otherwise set termSignal to nil before successful return. defer func() { if termSignal != nil { termSignal <- true } }() response, err := p.client.NetworkCreate(ctx, req.Name, nc) if err != nil { return &DockerNetwork{}, err } n := &DockerNetwork{ ID: response.ID, Driver: req.Driver, Name: req.Name, terminationSignal: termSignal, provider: p, } // Disable cleanup on success termSignal = nil return n, nil } // GetNetwork returns the object representing the network identified by its name func (p *DockerProvider) GetNetwork(ctx context.Context, req NetworkRequest) (types.NetworkResource, error) { networkResource, err := p.client.NetworkInspect(ctx, req.Name, types.NetworkInspectOptions{ Verbose: true, }) if err != nil { return types.NetworkResource{}, err } return networkResource, err } func (p *DockerProvider) GetGatewayIP(ctx context.Context) (string, error) { // Use a default network as defined in the DockerProvider if p.DefaultNetwork == "" { var err error p.DefaultNetwork, err = p.getDefaultNetwork(ctx, p.client) if err != nil { return "", err } } nw, err := p.GetNetwork(ctx, NetworkRequest{Name: p.DefaultNetwork}) if err != nil { return "", err } var ip string for _, config := range nw.IPAM.Config { if config.Gateway != "" { ip = config.Gateway break } } if ip == "" { return "", errors.New("Failed to get gateway IP from network settings") } return ip, nil } func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APIClient) (string, error) { // Get list of available networks networkResources, err := cli.NetworkList(ctx, types.NetworkListOptions{}) if err != nil { return "", err } reaperNetwork := ReaperDefault reaperNetworkExists := false for _, net := range networkResources { if net.Name == p.defaultBridgeNetworkName { return p.defaultBridgeNetworkName, nil } if net.Name == reaperNetwork { reaperNetworkExists = true } } // Create a bridge network for the container communications if !reaperNetworkExists { _, err = cli.NetworkCreate(ctx, reaperNetwork, types.NetworkCreate{ Driver: Bridge, Attachable: true, Labels: map[string]string{ TestcontainerLabel: "true", testcontainersdocker.LabelLang: "go", testcontainersdocker.LabelVersion: internal.Version, }, }) if err != nil { return "", err } } return reaperNetwork, nil }