Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,185 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/magiconair/properties"
)
const ReaperDefaultImage = "testcontainers/ryuk:0.13.0"
var (
tcConfig Config
tcConfigOnce = new(sync.Once)
)
// testcontainersConfig {
// Config represents the configuration for Testcontainers.
// User values are read from ~/.testcontainers.properties file which can be overridden
// using the specified environment variables. For more information, see [Custom Configuration].
//
// The Ryuk prefixed fields controls the [Garbage Collector] feature, which ensures that
// resources are cleaned up after the test execution.
//
// [Garbage Collector]: https://golang.testcontainers.org/features/garbage_collector/
// [Custom Configuration]: https://golang.testcontainers.org/features/configuration/
type Config struct {
// Host is the address of the Docker daemon.
//
// Environment variable: DOCKER_HOST
Host string `properties:"docker.host,default="`
// TLSVerify is a flag to enable or disable TLS verification when connecting to a Docker daemon.
//
// Environment variable: DOCKER_TLS_VERIFY
TLSVerify int `properties:"docker.tls.verify,default=0"`
// CertPath is the path to the directory containing the Docker certificates.
// This is used when connecting to a Docker daemon over TLS.
//
// Environment variable: DOCKER_CERT_PATH
CertPath string `properties:"docker.cert.path,default="`
// HubImageNamePrefix is the prefix used for the images pulled from the Docker Hub.
// This is useful when running tests in environments with restricted internet access.
//
// Environment variable: TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX
HubImageNamePrefix string `properties:"hub.image.name.prefix,default="`
// RyukDisabled is a flag to enable or disable the Garbage Collector.
// Setting this to true will prevent testcontainers from automatically cleaning up
// resources, which is particularly important in tests which timeout as they
// don't run test clean up.
//
// Environment variable: TESTCONTAINERS_RYUK_DISABLED
RyukDisabled bool `properties:"ryuk.disabled,default=false"`
// RyukPrivileged is a flag to enable or disable the privileged mode for the Garbage Collector container.
// Setting this to true will run the Garbage Collector container in privileged mode.
//
// Environment variable: TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED
RyukPrivileged bool `properties:"ryuk.container.privileged,default=false"`
// RyukReconnectionTimeout is the time to wait before attempting to reconnect to the Garbage Collector container.
//
// Environment variable: RYUK_RECONNECTION_TIMEOUT
RyukReconnectionTimeout time.Duration `properties:"ryuk.reconnection.timeout,default=10s"`
// RyukConnectionTimeout is the time to wait before timing out when connecting to the Garbage Collector container.
//
// Environment variable: RYUK_CONNECTION_TIMEOUT
RyukConnectionTimeout time.Duration `properties:"ryuk.connection.timeout,default=1m"`
// RyukVerbose is a flag to enable or disable verbose logging for the Garbage Collector.
//
// Environment variable: RYUK_VERBOSE
RyukVerbose bool `properties:"ryuk.verbose,default=false"`
// TestcontainersHost is the address of the Testcontainers host.
//
// Environment variable: TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE
TestcontainersHost string `properties:"tc.host,default="`
}
// }
// Read reads from testcontainers properties file, if it exists
// it is possible that certain values get overridden when set as environment variables
func Read() Config {
tcConfigOnce.Do(func() {
tcConfig = read()
})
return tcConfig
}
// Reset resets the singleton instance of the Config struct,
// allowing to read the configuration again.
// Handy for testing, so do not use it in production code
// This function is not thread-safe
func Reset() {
tcConfigOnce = new(sync.Once)
}
func read() Config {
config := Config{}
applyEnvironmentConfiguration := func(config Config) Config {
ryukDisabledEnv := os.Getenv("TESTCONTAINERS_RYUK_DISABLED")
if parseBool(ryukDisabledEnv) {
config.RyukDisabled = ryukDisabledEnv == "true"
}
hubImageNamePrefix := os.Getenv("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX")
if hubImageNamePrefix != "" {
config.HubImageNamePrefix = hubImageNamePrefix
}
ryukPrivilegedEnv := os.Getenv("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED")
if parseBool(ryukPrivilegedEnv) {
config.RyukPrivileged = ryukPrivilegedEnv == "true"
}
ryukVerboseEnv := readTestcontainersEnv("RYUK_VERBOSE")
if parseBool(ryukVerboseEnv) {
config.RyukVerbose = ryukVerboseEnv == "true"
}
ryukReconnectionTimeoutEnv := readTestcontainersEnv("RYUK_RECONNECTION_TIMEOUT")
if timeout, err := time.ParseDuration(ryukReconnectionTimeoutEnv); err == nil {
config.RyukReconnectionTimeout = timeout
}
ryukConnectionTimeoutEnv := readTestcontainersEnv("RYUK_CONNECTION_TIMEOUT")
if timeout, err := time.ParseDuration(ryukConnectionTimeoutEnv); err == nil {
config.RyukConnectionTimeout = timeout
}
return config
}
home, err := os.UserHomeDir()
if err != nil {
return applyEnvironmentConfiguration(config)
}
tcProp := filepath.Join(home, ".testcontainers.properties")
// init from a file
properties, err := properties.LoadFile(tcProp, properties.UTF8)
if err != nil {
return applyEnvironmentConfiguration(config)
}
if err := properties.Decode(&config); err != nil {
fmt.Printf("invalid testcontainers properties file, returning an empty Testcontainers configuration: %v\n", err)
return applyEnvironmentConfiguration(config)
}
return applyEnvironmentConfiguration(config)
}
func parseBool(input string) bool {
_, err := strconv.ParseBool(input)
return err == nil
}
// readTestcontainersEnv reads the environment variable with the given name.
// It checks for the environment variable with the given name first, and then
// checks for the environment variable with the given name prefixed with "TESTCONTAINERS_".
func readTestcontainersEnv(envVar string) string {
value := os.Getenv(envVar)
if value != "" {
return value
}
// TODO: remove this prefix after the next major release
const prefix string = "TESTCONTAINERS_"
return os.Getenv(prefix + envVar)
}
@@ -0,0 +1,106 @@
package core
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"github.com/google/uuid"
"github.com/shirou/gopsutil/v4/process"
)
// sessionID returns a unique session ID for the current test session. Because each Go package
// will be run in a separate process, we need a way to identify the current test session.
// By test session, we mean:
// - a single "go test" invocation (including flags)
// - a single "go test ./..." invocation (including flags)
// - the execution of a single test or a set of tests using the IDE
//
// As a consequence, with the sole goal of aggregating test execution across multiple
// packages, this function will use the parent process ID (pid) of the current process
// and its creation date, to use it to generate a unique session ID. We are using the parent pid because
// the current process will be a child process of:
// - the process that is running the tests, e.g.: "go test";
// - the process that is running the application in development mode, e.g. "go run main.go -tags dev";
// - the process that is running the tests in the IDE, e.g.: "go test ./...".
//
// Finally, we will hash the combination of the "testcontainers-go:" string with the parent pid
// and the creation date of that parent process to generate a unique session ID.
//
// This sessionID will be used to:
// - identify the test session, aggregating the test execution of multiple packages in the same test session.
// - tag the containers created by testcontainers-go, adding a label to the container with the session ID.
var sessionID string
// projectPath returns the current working directory of the parent test process running Testcontainers for Go.
// If it's not possible to get that directory, the library will use the current working directory. If again
// it's not possible to get the current working directory, the library will use a temporary directory.
var projectPath string
// processID returns a unique ID for the current test process. Because each Go package will be run in a separate process,
// we need a way to identify the current test process, in the form of a UUID
var processID string
const sessionIDPlaceholder = "testcontainers-go:%d:%d"
func init() {
processID = uuid.New().String()
parentPid := os.Getppid()
var createTime int64
fallbackCwd, err := os.Getwd()
if err != nil {
// very unlikely to fail, but if it does, we will use a temp dir
fallbackCwd = os.TempDir()
}
processes, err := process.Processes()
if err != nil {
sessionID = uuid.New().String()
projectPath = fallbackCwd
return
}
for _, p := range processes {
if int(p.Pid) != parentPid {
continue
}
cwd, err := p.Cwd()
if err != nil {
cwd = fallbackCwd
}
projectPath = cwd
t, err := p.CreateTime()
if err != nil {
sessionID = uuid.New().String()
return
}
createTime = t
break
}
hasher := sha256.New()
_, err = fmt.Fprintf(hasher, sessionIDPlaceholder, parentPid, createTime)
if err != nil {
sessionID = uuid.New().String()
return
}
sessionID = hex.EncodeToString(hasher.Sum(nil))
}
func ProcessID() string {
return processID
}
func ProjectPath() string {
return projectPath
}
func SessionID() string {
return sessionID
}
@@ -0,0 +1,53 @@
package core
import (
"context"
"path/filepath"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal"
"github.com/testcontainers/testcontainers-go/internal/config"
)
// NewClient returns a new docker client extracting the docker host from the different alternatives
func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) {
dockerHost, err := ExtractDockerHost(ctx)
if err != nil {
return nil, err
}
tcConfig := config.Read()
opts := []client.Opt{client.FromEnv}
if dockerHost != "" {
opts = append(opts, client.WithHost(dockerHost))
// for further information, read https://docs.docker.com/engine/security/protect-access/
if tcConfig.TLSVerify == 1 {
cacertPath := filepath.Join(tcConfig.CertPath, "ca.pem")
certPath := filepath.Join(tcConfig.CertPath, "cert.pem")
keyPath := filepath.Join(tcConfig.CertPath, "key.pem")
opts = append(opts, client.WithTLSClientConfig(cacertPath, certPath, keyPath))
}
}
opts = append(opts, client.WithHTTPHeaders(
map[string]string{
"x-tc-pp": ProjectPath(),
"x-tc-sid": SessionID(),
"User-Agent": "tc-go/" + internal.Version,
}),
)
// passed options have priority over the default ones
opts = append(opts, ops...)
cli, err := client.New(opts...)
if err != nil {
return nil, err
}
return cli, nil
}
@@ -0,0 +1,334 @@
package core
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"github.com/moby/moby/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
dockerHostErrCache error
dockerHostOnce sync.Once
)
var (
dockerSocketPathCache string
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
}
// dockerHostCheck Use a vanilla Docker client to check if the Docker host is reachable.
// It will avoid recursive calls to this function.
var dockerHostCheck = func(ctx context.Context, host string) error {
cli, err := client.New(client.FromEnv, client.WithHost(host))
if err != nil {
return fmt.Errorf("new client: %w", err)
}
defer cli.Close()
_, err = cli.Info(ctx, client.InfoOptions{})
if err != nil {
return fmt.Errorf("docker info: %w", err)
}
return nil
}
// MustExtractDockerHost 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, because the Docker host is not set, it panics.
func MustExtractDockerHost(ctx context.Context) string {
host, err := ExtractDockerHost(ctx)
if err != nil {
panic(err)
}
return host
}
func ExtractDockerHost(ctx context.Context) (string, error) {
dockerHostOnce.Do(func() {
dockerHostCache, dockerHostErrCache = extractDockerHost(ctx)
})
return dockerHostCache, dockerHostErrCache
}
// MustExtractDockerSocket 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().OperatingSystem 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 MustExtractDockerHost.
// 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)
//
// It panics if a Docker client cannot be created, or the Docker host cannot be discovered.
func MustExtractDockerSocket(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, error) {
dockerHostFns := []func(context.Context) (string, error){
testcontainersHostFromProperties,
dockerHostFromEnv,
dockerHostFromContext,
dockerSocketPath,
dockerHostFromProperties,
rootlessDockerSocketPath,
}
var errs []error
for _, dockerHostFn := range dockerHostFns {
dockerHost, err := dockerHostFn(ctx)
if err != nil {
if !isHostNotSet(err) {
errs = append(errs, err)
}
continue
}
if err = dockerHostCheck(ctx, dockerHost); err != nil {
errs = append(errs, fmt.Errorf("check host %q: %w", dockerHost, err))
continue
}
return dockerHost, nil
}
if len(errs) > 0 {
return "", errors.Join(errs...)
}
return "", ErrSocketNotFound
}
// extractDockerSocket 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.
// It panics if a Docker client cannot be created, or the Docker host is not discovered.
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.
// It panics if the Docker Info call errors, or the Docker host is not discovered.
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()
if err == nil {
return checkDockerSocketFn(testcontainersDockerSocket)
}
info, err := cli.Info(ctx, client.InfoOptions{})
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.Info.OperatingSystem == "Docker Desktop" {
if IsWindows() {
return WindowsDockerSocketPath
}
return DockerSocketPath
}
dockerHost, err := extractDockerHost(ctx)
if err != nil {
panic(err) // Docker host is required to get the Docker socket
}
return checkDockerSocketFn(dockerHost)
}
// isHostNotSet returns true if the error is related to the Docker host
// not being set, false otherwise.
func isHostNotSet(err error) bool {
switch {
case errors.Is(err, ErrTestcontainersHostNotSetInProperties),
errors.Is(err, ErrDockerHostNotSet),
errors.Is(err, ErrDockerSocketNotSetInContext),
errors.Is(err, ErrDockerSocketNotSetInProperties),
errors.Is(err, ErrSocketNotFoundInPath),
errors.Is(err, ErrXDGRuntimeDirNotSet),
errors.Is(err, ErrRootlessDockerNotFoundHomeRunDir),
errors.Is(err, ErrRootlessDockerNotFoundHomeDesktopDir),
errors.Is(err, ErrRootlessDockerNotFoundRunDir):
return true
default:
return false
}
}
// dockerHostFromEnv returns the docker host from the DOCKER_HOST environment variable, if it's not empty
func dockerHostFromEnv(_ 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(_ context.Context) (string, error) {
cfg := config.Read()
socketPath := cfg.Host
if socketPath != "" {
return socketPath, nil
}
return "", ErrDockerSocketNotSetInProperties
}
// dockerSocketOverridePath returns the docker socket from the TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable,
// if it's not empty
func dockerSocketOverridePath() (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(_ 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(_ context.Context) (string, error) {
cfg := config.Read()
testcontainersHost := cfg.TestcontainersHost
if testcontainersHost != "" {
// Validate the URL format
_, err := parseURL(testcontainersHost)
if err != nil {
return "", err
}
// Return the original URL to preserve schema for Docker client
return testcontainersHost, nil
}
return "", ErrTestcontainersHostNotSetInProperties
}
// DockerEnvFile is the file that is created when running inside a container.
// It's a variable to allow testing.
// TODO: Remove this once context rework is done, which eliminates need for the default network creation.
var DockerEnvFile = "/.dockerenv"
// 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(DockerEnvFile)
}
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
}
@@ -0,0 +1,150 @@
package core
import (
"context"
"errors"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
)
var (
ErrRootlessDockerNotFound = errors.New("rootless Docker not found")
ErrRootlessDockerNotFoundHomeDesktopDir = errors.New("checked path: ~/.docker/desktop/docker.sock")
ErrRootlessDockerNotFoundHomeRunDir = errors.New("checked path: ~/.docker/run/docker.sock")
ErrRootlessDockerNotFoundRunDir = errors.New("checked path: /run/user/${uid}/docker.sock")
ErrRootlessDockerNotFoundXDGRuntimeDir = errors.New("checked path: $XDG_RUNTIME_DIR")
ErrRootlessDockerNotSupportedWindows = errors.New("rootless Docker is not supported on Windows")
ErrXDGRuntimeDirNotSet = errors.New("XDG_RUNTIME_DIR is not set")
)
// baseRunDir is the base directory for the "/run/user/${uid}" directory.
// It is a variable so it can be modified for testing.
var baseRunDir = "/run"
// IsWindows returns if the current OS is Windows. For that it checks the GOOS environment variable or the runtime.GOOS constant.
func IsWindows() bool {
return os.Getenv("GOOS") == "windows" || runtime.GOOS == "windows"
}
// rootlessDockerSocketPath returns if the path to the rootless Docker socket exists.
// The rootless socket path is determined by the following order:
//
// 1. XDG_RUNTIME_DIR environment variable.
// 2. ~/.docker/run/docker.sock file.
// 3. ~/.docker/desktop/docker.sock file.
// 4. /run/user/${uid}/docker.sock file.
// 5. Else, return ErrRootlessDockerNotFound, wrapping specific errors for each of the above paths.
//
// It should include the Docker socket schema (unix://) in the returned path.
func rootlessDockerSocketPath(_ context.Context) (string, error) {
// adding a manner to test it on non-windows machines, setting the GOOS env var to windows
// This is needed because runtime.GOOS is a constant that returns the OS of the machine running the test
if IsWindows() {
return "", ErrRootlessDockerNotSupportedWindows
}
socketPathFns := []func() (string, error){
rootlessSocketPathFromEnv,
rootlessSocketPathFromHomeRunDir,
rootlessSocketPathFromHomeDesktopDir,
rootlessSocketPathFromRunDir,
}
var errs []error
for _, socketPathFn := range socketPathFns {
s, err := socketPathFn()
if err != nil {
if !isHostNotSet(err) {
errs = append(errs, err)
}
continue
}
return DockerSocketSchema + s, nil
}
if len(errs) > 0 {
return "", errors.Join(errs...)
}
return "", ErrRootlessDockerNotFound
}
func fileExists(f string) bool {
_, err := os.Stat(f)
return err == nil
}
func parseURL(s string) (string, error) {
hostURL, err := url.Parse(s)
if err != nil {
return "", err
}
switch hostURL.Scheme {
case "unix", "npipe":
return hostURL.Path, nil
case "tcp":
// return the original URL, as it is a valid TCP URL
return s, nil
default:
return "", ErrNoUnixSchema
}
}
// rootlessSocketPathFromEnv returns the path to the rootless Docker socket from the XDG_RUNTIME_DIR environment variable.
// It should include the Docker socket schema (unix://) in the returned path.
func rootlessSocketPathFromEnv() (string, error) {
xdgRuntimeDir, exists := os.LookupEnv("XDG_RUNTIME_DIR")
if exists {
f := filepath.Join(xdgRuntimeDir, "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundXDGRuntimeDir
}
return "", ErrXDGRuntimeDirNotSet
}
// rootlessSocketPathFromHomeRunDir returns the path to the rootless Docker socket from the ~/.docker/run/docker.sock file.
func rootlessSocketPathFromHomeRunDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
f := filepath.Join(home, ".docker", "run", "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundHomeRunDir
}
// rootlessSocketPathFromHomeDesktopDir returns the path to the rootless Docker socket from the ~/.docker/desktop/docker.sock file.
func rootlessSocketPathFromHomeDesktopDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
f := filepath.Join(home, ".docker", "desktop", "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundHomeDesktopDir
}
// rootlessSocketPathFromRunDir returns the path to the rootless Docker socket from the /run/user/<uid>/docker.sock file.
func rootlessSocketPathFromRunDir() (string, error) {
uid := os.Getuid()
f := filepath.Join(baseRunDir, "user", strconv.Itoa(uid), "docker.sock")
if fileExists(f) {
return f, nil
}
return "", ErrRootlessDockerNotFoundRunDir
}
@@ -0,0 +1,49 @@
package core
import (
"net/url"
"strings"
"github.com/moby/moby/client"
)
// DockerSocketSchema is the unix schema.
var DockerSocketSchema = "unix://"
// DockerSocketPath is the path to the docker socket under unix systems.
var DockerSocketPath = "/var/run/docker.sock"
// DockerSocketPathWithSchema is the path to the docker socket under unix systems with the unix schema.
var DockerSocketPathWithSchema = DockerSocketSchema + DockerSocketPath
// TCPSchema is the tcp schema.
var TCPSchema = "tcp://"
// WindowsDockerSocketPath is the path to the docker socket under windows systems.
var WindowsDockerSocketPath = "//var/run/docker.sock"
func init() {
const DefaultDockerHost = client.DefaultDockerHost
u, err := url.Parse(DefaultDockerHost)
if err != nil {
// unsupported default host specified by the docker client package,
// so revert to the default unix docker socket path
return
}
switch u.Scheme {
case "unix", "npipe":
DockerSocketSchema = u.Scheme + "://"
DockerSocketPath = u.Path
if !strings.HasPrefix(DockerSocketPath, "/") {
// seeing as the code in this module depends on DockerSocketPath having
// a slash (`/`) prefix, we add it here if it is missing.
// for the known environments, we do not foresee how the socket-path
// should miss the slash, however this extra if-condition is worth to
// save future pain from innocent users.
DockerSocketPath = "/" + DockerSocketPath
}
DockerSocketPathWithSchema = DockerSocketSchema + DockerSocketPath
}
}
@@ -0,0 +1,143 @@
package core
import (
"bufio"
"io"
"net/url"
"os"
"regexp"
"strings"
"unicode/utf8"
)
const (
IndexDockerIO = "https://index.docker.io/v1/"
maxURLRuneCount = 2083
minURLRuneCount = 3
URLSchema = `((ftp|tcp|udp|wss?|https?):\/\/)`
URLUsername = `(\S+(:\S*)?@)`
URLIP = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
IP = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
URLSubdomain = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
URLPath = `((\/|\?|#)[^\s]*)`
URLPort = `(:(\d{1,5}))`
URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
)
var rxURL = regexp.MustCompile(URL)
// ExtractImagesFromDockerfile extracts images from the Dockerfile sourced from dockerfile.
func ExtractImagesFromDockerfile(dockerfile string, buildArgs map[string]*string) ([]string, error) {
file, err := os.Open(dockerfile)
if err != nil {
return nil, err
}
defer file.Close()
return ExtractImagesFromReader(file, buildArgs)
}
// ExtractImagesFromReader extracts images from the Dockerfile sourced from r.
func ExtractImagesFromReader(r io.Reader, buildArgs map[string]*string) ([]string, error) {
var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
images := make([]string, 0, len(lines))
// extract images from dockerfile
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(strings.ToUpper(line), "FROM") {
continue
}
// remove FROM
line = strings.TrimPrefix(line, "FROM")
parts := strings.Split(strings.TrimSpace(line), " ")
if len(parts) == 0 {
continue
}
// interpolate build args
for k, v := range buildArgs {
if v != nil {
parts[0] = strings.ReplaceAll(parts[0], "${"+k+"}", *v)
}
}
images = append(images, parts[0])
}
return images, nil
}
// ExtractRegistry extracts the registry from the image name, using a regular expression to extract the registry from the image name.
// regular expression to extract the registry from the image name
// the regular expression is based on the grammar defined in
// - image:tag
// - image
// - repository/image:tag
// - repository/image
// - registry/image:tag
// - registry/image
// - registry/repository/image:tag
// - registry/repository/image
// - registry:port/repository/image:tag
// - registry:port/repository/image
// - registry:port/image:tag
// - registry:port/image
// Once extracted the registry, it is validated to check if it is a valid URL or an IP address.
func ExtractRegistry(image string, fallback string) string {
exp := regexp.MustCompile(`^(?:(?P<registry>(https?://)?[^/]+)(?::(?P<port>\d+))?/)?(?:(?P<repository>[^/]+)/)?(?P<image>[^:]+)(?::(?P<tag>.+))?$`).FindStringSubmatch(image)
if len(exp) == 0 {
return ""
}
registry := exp[1]
// docker.io is an implicit reference, return fallback for normalization
if strings.EqualFold(registry, "docker.io") {
return fallback
}
// registry.hub.docker.com is an explicit registry reference, preserve it
if strings.EqualFold(registry, "registry.hub.docker.com") {
return "registry.hub.docker.com"
}
if IsURL(registry) {
return registry
}
return fallback
}
// IsURL checks if the string is a URL.
// Extracted from https://github.com/asaskevich/govalidator/blob/f21760c49a8d/validator.go#L104
func IsURL(str string) bool {
if str == "" || utf8.RuneCountInString(str) >= maxURLRuneCount || len(str) <= minURLRuneCount || strings.HasPrefix(str, ".") {
return false
}
strTemp := str
if strings.Contains(str, ":") && !strings.Contains(str, "://") {
// support no indicated urlscheme but with colon for port number
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
strTemp = "http://" + str
}
u, err := url.Parse(strTemp)
if err != nil {
return false
}
if strings.HasPrefix(u.Host, ".") {
return false
}
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
return false
}
return rxURL.MatchString(str)
}
@@ -0,0 +1,72 @@
package core
import (
"errors"
"fmt"
"maps"
"strings"
"github.com/testcontainers/testcontainers-go/internal"
"github.com/testcontainers/testcontainers-go/internal/config"
)
const (
// LabelBase is the base label for all testcontainers labels.
LabelBase = "org.testcontainers"
// LabelLang specifies the language which created the test container.
LabelLang = LabelBase + ".lang"
// LabelReaper identifies the container as a reaper.
LabelReaper = LabelBase + ".reaper"
// LabelRyuk identifies the container as a ryuk.
LabelRyuk = LabelBase + ".ryuk"
// LabelSessionID specifies the session ID of the container.
LabelSessionID = LabelBase + ".sessionId"
// LabelVersion specifies the version of testcontainers which created the container.
LabelVersion = LabelBase + ".version"
// LabelReap specifies the container should be reaped by the reaper.
LabelReap = LabelBase + ".reap"
)
// DefaultLabels returns the standard set of labels which
// includes LabelSessionID if the reaper is enabled.
func DefaultLabels(sessionID string) map[string]string {
labels := map[string]string{
LabelBase: "true",
LabelLang: "go",
LabelVersion: internal.Version,
LabelSessionID: sessionID,
}
if !config.Read().RyukDisabled {
labels[LabelReap] = "true"
}
return labels
}
// AddDefaultLabels adds the default labels for sessionID to target.
func AddDefaultLabels(sessionID string, target map[string]string) {
maps.Copy(target, DefaultLabels(sessionID))
}
// MergeCustomLabels sets labels from src to dst.
// If a key in src has [LabelBase] prefix returns an error.
// If dst is nil returns an error.
func MergeCustomLabels(dst, src map[string]string) error {
if dst == nil {
return errors.New("destination map is nil")
}
for key := range src {
if strings.HasPrefix(key, LabelBase) {
return fmt.Errorf("key %q has %q prefix", key, LabelBase)
}
}
maps.Copy(dst, src)
return nil
}
@@ -0,0 +1,52 @@
package network
import (
"context"
"fmt"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go/internal/core"
)
const (
// FilterByID uses to filter network by identifier.
FilterByID = "id"
// FilterByName uses to filter network by name.
FilterByName = "name"
)
// Get returns a network by its ID.
func Get(ctx context.Context, id string) (network.Summary, error) {
return get(ctx, FilterByID, id)
}
// GetByName returns a network by its name.
func GetByName(ctx context.Context, name string) (network.Summary, error) {
return get(ctx, FilterByName, name)
}
func get(ctx context.Context, filter string, value string) (network.Summary, error) {
var nw network.Summary // initialize to the zero value
cli, err := core.NewClient(ctx)
if err != nil {
return nw, err
}
defer cli.Close()
list, err := cli.NetworkList(ctx, client.NetworkListOptions{
Filters: make(client.Filters).Add(filter, value),
})
if err != nil {
return nw, fmt.Errorf("failed to list networks: %w", err)
}
if len(list.Items) == 0 {
return nw, fmt.Errorf("network %s not found (filtering by %s)", value, filter)
}
return list.Items[0], nil
}
@@ -0,0 +1,4 @@
package internal
// Version is the next development version of the application
const Version = "0.42.0"