robocar-base/vendor/github.com/testcontainers/testcontainers-go/internal/testcontainersdocker/docker_host.go

270 lines
9.5 KiB
Go
Raw Normal View History

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
}