diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
| commit | 60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch) | |
| tree | 2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/testcontainers/testcontainers-go/internal | |
| parent | 05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff) | |
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/testcontainers/testcontainers-go/internal')
10 files changed, 1130 insertions, 0 deletions
diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go b/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go new file mode 100644 index 0000000..64f2f7f --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go @@ -0,0 +1,185 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "sync" + "time" + + "github.com/magiconair/properties" +) + +const ReaperDefaultImage = "testcontainers/ryuk:0.11.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) +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/bootstrap.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/bootstrap.go new file mode 100644 index 0000000..201d4b0 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/bootstrap.go @@ -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 = hasher.Write([]byte(fmt.Sprintf(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 +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go new file mode 100644 index 0000000..04a54bc --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go @@ -0,0 +1,50 @@ +package core + +import ( + "context" + "path/filepath" + + "github.com/docker/docker/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) { + tcConfig := config.Read() + + dockerHost := MustExtractDockerHost(ctx) + + opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()} + 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.NewClientWithOpts(opts...) + if err != nil { + return nil, err + } + + return cli, nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_host.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_host.go new file mode 100644 index 0000000..fc06ea8 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_host.go @@ -0,0 +1,329 @@ +package core + +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 + 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.NewClientWithOpts(client.FromEnv, client.WithHost(host), client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("new client: %w", err) + } + defer cli.Close() + + _, err = cli.Info(ctx) + 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 { + dockerHostOnce.Do(func() { + cache, err := extractDockerHost(ctx) + if err != nil { + panic(err) + } + + dockerHostCache = cache + }) + + return dockerHostCache +} + +// 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) + 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, 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 != "" { + parsed, err := parseURL(testcontainersHost) + if err != nil { + return "", err + } + + return parsed, 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 +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_rootless.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_rootless.go new file mode 100644 index 0000000..8108384 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_rootless.go @@ -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 +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_socket.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_socket.go new file mode 100644 index 0000000..b0c0c84 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_socket.go @@ -0,0 +1,49 @@ +package core + +import ( + "net/url" + "strings" + + "github.com/docker/docker/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 + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/images.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/images.go new file mode 100644 index 0000000..f073a90 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/images.go @@ -0,0 +1,132 @@ +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 images []string + var lines []string + scanner := bufio.NewScanner(r) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if scanner.Err() != nil { + return nil, scanner.Err() + } + + // 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] + + 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) +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go new file mode 100644 index 0000000..0814924 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go @@ -0,0 +1,73 @@ +package core + +import ( + "errors" + "fmt" + "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) { + for k, v := range DefaultLabels(sessionID) { + target[k] = v + } +} + +// 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, value := range src { + if strings.HasPrefix(key, LabelBase) { + return fmt.Errorf("key %q has %q prefix", key, LabelBase) + } + dst[key] = value + } + return nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/network/network.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/network/network.go new file mode 100644 index 0000000..787065a --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/network/network.go @@ -0,0 +1,52 @@ +package network + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + + "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.Inspect, error) { + return get(ctx, FilterByID, id) +} + +// GetByName returns a network by its name. +func GetByName(ctx context.Context, name string) (network.Inspect, error) { + return get(ctx, FilterByName, name) +} + +func get(ctx context.Context, filter string, value string) (network.Inspect, error) { + var nw network.Inspect // initialize to the zero value + + cli, err := core.NewClient(ctx) + if err != nil { + return nw, err + } + defer cli.Close() + + list, err := cli.NetworkList(ctx, network.ListOptions{ + Filters: filters.NewArgs(filters.Arg(filter, value)), + }) + if err != nil { + return nw, fmt.Errorf("failed to list networks: %w", err) + } + + if len(list) == 0 { + return nw, fmt.Errorf("network %s not found (filtering by %s)", value, filter) + } + + return list[0], nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/version.go b/vendor/github.com/testcontainers/testcontainers-go/internal/version.go new file mode 100644 index 0000000..7e6da64 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/version.go @@ -0,0 +1,4 @@ +package internal + +// Version is the next development version of the application +const Version = "0.36.0" |
