270 lines
9.5 KiB
Go
270 lines
9.5 KiB
Go
|
package testcontainersdocker
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/docker/docker/client"
|
||
|
"github.com/testcontainers/testcontainers-go/internal/config"
|
||
|
)
|
||
|
|
||
|
type dockerHostContext string
|
||
|
|
||
|
var DockerHostContextKey = dockerHostContext("docker_host")
|
||
|
|
||
|
var (
|
||
|
ErrDockerHostNotSet = errors.New("DOCKER_HOST is not set")
|
||
|
ErrDockerSocketOverrideNotSet = errors.New("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE is not set")
|
||
|
ErrDockerSocketNotSetInContext = errors.New("socket not set in context")
|
||
|
ErrDockerSocketNotSetInProperties = errors.New("socket not set in ~/.testcontainers.properties")
|
||
|
ErrNoUnixSchema = errors.New("URL schema is not unix")
|
||
|
ErrSocketNotFound = errors.New("socket not found")
|
||
|
ErrSocketNotFoundInPath = errors.New("docker socket not found in " + DockerSocketPath)
|
||
|
// ErrTestcontainersHostNotSetInProperties this error is specific to Testcontainers
|
||
|
ErrTestcontainersHostNotSetInProperties = errors.New("tc.host not set in ~/.testcontainers.properties")
|
||
|
)
|
||
|
|
||
|
var dockerHostCache string
|
||
|
var dockerHostOnce sync.Once
|
||
|
|
||
|
var dockerSocketPathCache string
|
||
|
var dockerSocketPathOnce sync.Once
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// ExtractDockerHost Extracts the docker host from the different alternatives, caching the result to avoid unnecessary
|
||
|
// calculations. Use this function to get the actual Docker host. This function does not consider Windows containers at the moment.
|
||
|
// The possible alternatives are:
|
||
|
//
|
||
|
// 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file.
|
||
|
// 2. DOCKER_HOST environment variable.
|
||
|
// 3. Docker host from context.
|
||
|
// 4. Docker host from the default docker socket path, without the unix schema.
|
||
|
// 5. Docker host from the "docker.host" property in the ~/.testcontainers.properties file.
|
||
|
// 6. Rootless docker socket path.
|
||
|
// 7. Else, the default Docker socket including schema will be returned.
|
||
|
func ExtractDockerHost(ctx context.Context) string {
|
||
|
dockerHostOnce.Do(func() {
|
||
|
dockerHostCache = extractDockerHost(ctx)
|
||
|
})
|
||
|
|
||
|
return dockerHostCache
|
||
|
}
|
||
|
|
||
|
// ExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema and
|
||
|
// caching the result to avoid unnecessary calculations. Use this function to get the docker socket path,
|
||
|
// not the host (e.g. mounting the socket in a container). This function does not consider Windows containers at the moment.
|
||
|
// The possible alternatives are:
|
||
|
//
|
||
|
// 1. Docker host from the "tc.host" property in the ~/.testcontainers.properties file.
|
||
|
// 2. The TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable.
|
||
|
// 3. Using a Docker client, check if the Info().OperativeSystem is "Docker Desktop" and return the default docker socket path for rootless docker.
|
||
|
// 4. Else, Get the current Docker Host from the existing strategies: see ExtractDockerHost.
|
||
|
// 5. If the socket contains the unix schema, the schema is removed (e.g. unix:///var/run/docker.sock -> /var/run/docker.sock)
|
||
|
// 6. Else, the default location of the docker socket is used (/var/run/docker.sock)
|
||
|
//
|
||
|
// In any case, if the docker socket schema is "tcp://", the default docker socket path will be returned.
|
||
|
func ExtractDockerSocket(ctx context.Context) string {
|
||
|
dockerSocketPathOnce.Do(func() {
|
||
|
dockerSocketPathCache = extractDockerSocket(ctx)
|
||
|
})
|
||
|
|
||
|
return dockerSocketPathCache
|
||
|
}
|
||
|
|
||
|
// extractDockerHost Extracts the docker host from the different alternatives, without caching the result.
|
||
|
// This internal method is handy for testing purposes.
|
||
|
func extractDockerHost(ctx context.Context) string {
|
||
|
dockerHostFns := []func(context.Context) (string, error){
|
||
|
testcontainersHostFromProperties,
|
||
|
dockerHostFromEnv,
|
||
|
dockerHostFromContext,
|
||
|
dockerSocketPath,
|
||
|
dockerHostFromProperties,
|
||
|
rootlessDockerSocketPath,
|
||
|
}
|
||
|
|
||
|
outerErr := ErrSocketNotFound
|
||
|
for _, dockerHostFn := range dockerHostFns {
|
||
|
dockerHost, err := dockerHostFn(ctx)
|
||
|
if err != nil {
|
||
|
outerErr = fmt.Errorf("%w: %v", outerErr, err)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
return dockerHost
|
||
|
}
|
||
|
|
||
|
// We are not supporting Windows containers at the moment
|
||
|
return DockerSocketPathWithSchema
|
||
|
}
|
||
|
|
||
|
// extractDockerHost Extracts the docker socket from the different alternatives, without caching the result.
|
||
|
// It will internally use the default Docker client, calling the internal method extractDockerSocketFromClient with it.
|
||
|
// This internal method is handy for testing purposes.
|
||
|
// If a Docker client cannot be created, the program will panic.
|
||
|
func extractDockerSocket(ctx context.Context) string {
|
||
|
cli, err := NewClient(ctx)
|
||
|
if err != nil {
|
||
|
panic(err) // a Docker client is required to get the Docker info
|
||
|
}
|
||
|
defer cli.Close()
|
||
|
|
||
|
return extractDockerSocketFromClient(ctx, cli)
|
||
|
}
|
||
|
|
||
|
// extractDockerSocketFromClient Extracts the docker socket from the different alternatives, without caching the result,
|
||
|
// and receiving an instance of the Docker API client interface.
|
||
|
// This internal method is handy for testing purposes, passing a mock type simulating the desired behaviour.
|
||
|
func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) string {
|
||
|
// check that the socket is not a tcp or unix socket
|
||
|
checkDockerSocketFn := func(socket string) string {
|
||
|
// this use case will cover the case when the docker host is a tcp socket
|
||
|
if strings.HasPrefix(socket, TCPSchema) {
|
||
|
return DockerSocketPath
|
||
|
}
|
||
|
|
||
|
if strings.HasPrefix(socket, DockerSocketSchema) {
|
||
|
return strings.Replace(socket, DockerSocketSchema, "", 1)
|
||
|
}
|
||
|
|
||
|
return socket
|
||
|
}
|
||
|
|
||
|
tcHost, err := testcontainersHostFromProperties(ctx)
|
||
|
if err == nil {
|
||
|
return checkDockerSocketFn(tcHost)
|
||
|
}
|
||
|
|
||
|
testcontainersDockerSocket, err := dockerSocketOverridePath(ctx)
|
||
|
if err == nil {
|
||
|
return checkDockerSocketFn(testcontainersDockerSocket)
|
||
|
}
|
||
|
|
||
|
info, err := cli.Info(ctx)
|
||
|
if err != nil {
|
||
|
panic(err) // Docker Info is required to get the Operating System
|
||
|
}
|
||
|
|
||
|
// Because Docker Desktop runs in a VM, we need to use the default docker path for rootless docker
|
||
|
if info.OperatingSystem == "Docker Desktop" {
|
||
|
if IsWindows() {
|
||
|
return WindowsDockerSocketPath
|
||
|
}
|
||
|
|
||
|
return DockerSocketPath
|
||
|
}
|
||
|
|
||
|
dockerHost := extractDockerHost(ctx)
|
||
|
|
||
|
return checkDockerSocketFn(dockerHost)
|
||
|
}
|
||
|
|
||
|
// dockerHostFromEnv returns the docker host from the DOCKER_HOST environment variable, if it's not empty
|
||
|
func dockerHostFromEnv(ctx context.Context) (string, error) {
|
||
|
if dockerHostPath := os.Getenv("DOCKER_HOST"); dockerHostPath != "" {
|
||
|
return dockerHostPath, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrDockerHostNotSet
|
||
|
}
|
||
|
|
||
|
// dockerHostFromContext returns the docker host from the Go context, if it's not empty
|
||
|
func dockerHostFromContext(ctx context.Context) (string, error) {
|
||
|
if socketPath, ok := ctx.Value(DockerHostContextKey).(string); ok && socketPath != "" {
|
||
|
parsed, err := parseURL(socketPath)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return parsed, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrDockerSocketNotSetInContext
|
||
|
}
|
||
|
|
||
|
// dockerHostFromProperties returns the docker host from the ~/.testcontainers.properties file, if it's not empty
|
||
|
func dockerHostFromProperties(ctx context.Context) (string, error) {
|
||
|
cfg := config.Read()
|
||
|
socketPath := cfg.Host
|
||
|
if socketPath != "" {
|
||
|
parsed, err := parseURL(socketPath)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return parsed, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrDockerSocketNotSetInProperties
|
||
|
}
|
||
|
|
||
|
// dockerSocketOverridePath returns the docker socket from the TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable,
|
||
|
// if it's not empty
|
||
|
func dockerSocketOverridePath(ctx context.Context) (string, error) {
|
||
|
if dockerHostPath, exists := os.LookupEnv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"); exists {
|
||
|
return dockerHostPath, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrDockerSocketOverrideNotSet
|
||
|
}
|
||
|
|
||
|
// dockerSocketPath returns the docker socket from the default docker socket path, if it's not empty
|
||
|
// and the socket exists
|
||
|
func dockerSocketPath(ctx context.Context) (string, error) {
|
||
|
if fileExists(DockerSocketPath) {
|
||
|
return DockerSocketPathWithSchema, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrSocketNotFoundInPath
|
||
|
}
|
||
|
|
||
|
// testcontainersHostFromProperties returns the testcontainers host from the ~/.testcontainers.properties file, if it's not empty
|
||
|
func testcontainersHostFromProperties(ctx context.Context) (string, error) {
|
||
|
cfg := config.Read()
|
||
|
testcontainersHost := cfg.TestcontainersHost
|
||
|
if testcontainersHost != "" {
|
||
|
parsed, err := parseURL(testcontainersHost)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
return parsed, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrTestcontainersHostNotSetInProperties
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|