Skip to content

Commit

Permalink
chore: restructure Docker helper methods (#799)
Browse files Browse the repository at this point in the history
* chore: add InAContainer

* chore: use new internal method for inAContainer

* chore: extract default gateway calculation to the internal package

* chore: move sessionID to the internal packages

* chore: simplify getting the string from the session ID

* chore: expose DaemonHost in the Docker provider

It will allow example modules to consume it

* fix: handle error in tests
  • Loading branch information
mdelapenya authored Feb 1, 2023
1 parent 10cdf3a commit a201e1d
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 60 deletions.
59 changes: 17 additions & 42 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"io"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
Expand All @@ -35,6 +34,7 @@ import (

tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/internal/testcontainerssession"
"github.com/testcontainers/testcontainers-go/wait"
)

Expand Down Expand Up @@ -133,7 +133,7 @@ func (c *DockerContainer) PortEndpoint(ctx context.Context, port nat.Port, proto
// 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)
host, err := c.provider.DaemonHost(ctx)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -782,7 +782,7 @@ func NewDockerClient() (cli *client.Client, host string, tcConfig TestContainers

opts = append(opts, client.WithHTTPHeaders(
map[string]string{
"x-tc-sid": sessionID().String(),
"x-tc-sid": testcontainerssession.String(),
}),
)

Expand Down Expand Up @@ -971,8 +971,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
req.Labels = make(map[string]string)
}

sessionID := sessionID()

reaperOpts := containerOptions{
ImageName: req.ReaperImage,
}
Expand All @@ -984,7 +982,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
// the reaper does not need to start a reaper for itself
isReaperContainer := strings.EqualFold(req.Image, reaperImage(reaperOpts.ImageName))
if !req.SkipReaper && !isReaperContainer {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID.String(), p, req.ReaperOptions...)
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)
}
Expand Down Expand Up @@ -1154,7 +1152,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
WaitingFor: req.WaitingFor,
Image: tag,
imageWasBuilt: req.ShouldBuildImage(),
sessionID: sessionID,
sessionID: testcontainerssession.ID(),
provider: p,
terminationSignal: termSignal,
skipReaper: req.SkipReaper,
Expand Down Expand Up @@ -1198,10 +1196,9 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
return p.CreateContainer(ctx, req)
}

sessionID := sessionID()
var termSignal chan bool
if !req.SkipReaper {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID.String(), p, req.ReaperOptions...)
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)
}
Expand All @@ -1216,7 +1213,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
ID: c.ID,
WaitingFor: req.WaitingFor,
Image: c.Image,
sessionID: sessionID,
sessionID: testcontainerssession.ID(),
provider: p,
terminationSignal: termSignal,
skipReaper: req.SkipReaper,
Expand Down Expand Up @@ -1283,10 +1280,14 @@ func (p *DockerProvider) Config() TestContainersConfig {
return p.config
}

// daemonHost gets the host or ip of the Docker daemon where ports are exposed on
// 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) {
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
}
Expand All @@ -1307,11 +1308,10 @@ func (p *DockerProvider) daemonHost(ctx context.Context) (string, error) {
case "http", "https", "tcp":
p.hostCache = url.Hostname()
case "unix", "npipe":
if inAContainer() {
if testcontainersdocker.InAContainer() {
ip, err := p.GetGatewayIP(ctx)
if err != nil {
// fallback to getDefaultGatewayIP
ip, err = getDefaultGatewayIP()
ip, err = testcontainersdocker.DefaultGatewayIP()
if err != nil {
ip = "localhost"
}
Expand All @@ -1321,7 +1321,7 @@ func (p *DockerProvider) daemonHost(ctx context.Context) (string, error) {
p.hostCache = "localhost"
}
default:
return "", errors.New("Could not determine host through env or docker host")
return "", errors.New("could not determine host through env or docker host")
}

return p.hostCache, nil
Expand Down Expand Up @@ -1355,8 +1355,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)

var termSignal chan bool
if !req.SkipReaper {
sessionID := sessionID()
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID.String(), p, req.ReaperOptions...)
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)
}
Expand Down Expand Up @@ -1438,30 +1437,6 @@ func (p *DockerProvider) printReaperBanner(resource string) {
p.Logger.Printf(ryukDisabledMessage)
}

func inAContainer() bool {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L15
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
return false
}

// deprecated
// see https://github.com/testcontainers/testcontainers-java/blob/main/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L46
func getDefaultGatewayIP() (string, error) {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L27
cmd := exec.Command("sh", "-c", "ip route|awk '/default/ { print $3 }'")
stdout, err := cmd.Output()
if err != nil {
return "", errors.New("Failed to detect docker host")
}
ip := strings.TrimSpace(string(stdout))
if len(ip) == 0 {
return "", errors.New("Failed to parse default gateway IP")
}
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{})
Expand Down
33 changes: 33 additions & 0 deletions internal/testcontainersdocker/docker_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ package testcontainersdocker

import (
"context"
"errors"
"net/url"
"os"
"os/exec"
"strings"
)

type dockerHostContext string

var DockerHostContextKey = dockerHostContext("docker_host")

// deprecated
// see https://github.com/testcontainers/testcontainers-java/blob/main/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L46
func DefaultGatewayIP() (string, error) {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L27
cmd := exec.Command("sh", "-c", "ip route|awk '/default/ { print $3 }'")
stdout, err := cmd.Output()
if err != nil {
return "", errors.New("failed to detect docker host")
}
ip := strings.TrimSpace(string(stdout))
if len(ip) == 0 {
return "", errors.New("failed to parse default gateway IP")
}
return ip, nil
}

// Extracts the docker host from the context, or returns the default value
func ExtractDockerHost(ctx context.Context) (dockerHostPath string) {
if dockerHostPath = os.Getenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"); dockerHostPath != "" {
Expand Down Expand Up @@ -38,3 +57,17 @@ func ExtractDockerHost(ctx context.Context) (dockerHostPath string) {
return dockerHostPath
}
}

// InAContainer returns true if the code is running inside a container
// See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25
func InAContainer() bool {
return inAContainer("/.dockerenv")
}

func inAContainer(path string) bool {
// see https://github.com/testcontainers/testcontainers-java/blob/3ad8d80e2484864e554744a4800a81f6b7982168/core/src/main/java/org/testcontainers/dockerclient/DockerClientConfigUtils.java#L15
if _, err := os.Stat(path); err == nil {
return true
}
return false
}
22 changes: 22 additions & 0 deletions internal/testcontainersdocker/docker_host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package testcontainersdocker

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -45,3 +47,23 @@ func Test_ExtractDockerHost(t *testing.T) {
assert.Equal(t, "/this/is/a/sample.sock", host)
})
}

func TestInAContainer(t *testing.T) {
const dockerenvName = ".dockerenv"

t.Run("file does not exist", func(t *testing.T) {
tmpDir := t.TempDir()

assert.False(t, inAContainer(filepath.Join(tmpDir, dockerenvName)))
})

t.Run("file exists", func(t *testing.T) {
tmpDir := t.TempDir()

f := filepath.Join(tmpDir, dockerenvName)

_, err := os.Create(f)
assert.NoError(t, err)
assert.True(t, inAContainer(f))
})
}
22 changes: 22 additions & 0 deletions internal/testcontainerssession/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package testcontainerssession

import (
"sync"

"github.com/google/uuid"
)

var id uuid.UUID
var idOnce sync.Once

func ID() uuid.UUID {
idOnce.Do(func() {
id = uuid.New()
})

return id
}

func String() string {
return ID().String()
}
18 changes: 0 additions & 18 deletions session.go

This file was deleted.

0 comments on commit a201e1d

Please sign in to comment.