From 66c387f392bec80566d937fa0f22ae7afec537e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:08:26 +0000 Subject: [PATCH] fix(deps): update module github.com/testcontainers/testcontainers-go to v0.34.0 --- go.mod | 8 +- go.sum | 4 + vendor/github.com/cpuguy83/dockercfg/auth.go | 127 ++-- .../github.com/cpuguy83/dockercfg/config.go | 10 +- vendor/github.com/cpuguy83/dockercfg/load.go | 22 +- .../testcontainers-go/.gitignore | 5 +- .../testcontainers-go/.golangci.yml | 16 +- .../testcontainers-go/.mockery.yaml | 11 + .../testcontainers/testcontainers-go/Pipfile | 4 +- .../testcontainers-go/Pipfile.lock | 222 +++---- .../testcontainers-go/README.md | 19 +- .../testcontainers-go/cleanup.go | 107 ++++ .../testcontainers-go/commons-test.mk | 17 +- .../testcontainers-go/container.go | 33 +- .../testcontainers-go/docker.go | 390 ++++++------ .../testcontainers-go/docker_auth.go | 49 +- .../testcontainers-go/docker_client.go | 4 +- .../testcontainers-go/docker_mounts.go | 4 +- .../testcontainers-go/generate.go | 3 + .../testcontainers-go/generic.go | 21 +- .../internal/config/config.go | 2 +- .../testcontainers-go/internal/core/client.go | 2 +- .../internal/core/docker_host.go | 89 ++- .../internal/core/docker_rootless.go | 12 +- .../testcontainers-go/internal/core/labels.go | 64 +- .../testcontainers-go/internal/version.go | 2 +- .../testcontainers-go/lifecycle.go | 7 +- .../testcontainers-go/mkdocs.yml | 8 +- .../testcontainers-go/options.go | 15 +- .../testcontainers-go/parallel.go | 71 +-- .../testcontainers-go/port_forwarding.go | 67 +- .../testcontainers-go/provider.go | 2 +- .../testcontainers-go/reaper.go | 587 +++++++++++------- .../sonar-project.properties | 4 +- .../testcontainers-go/testcontainers.go | 15 +- .../testdata/.docker/config.json | 8 - .../testcontainers-go/testing.go | 100 +++ .../testcontainers-go/wait/file.go | 112 ++++ .../testcontainers-go/wait/host_port.go | 53 +- .../testcontainers-go/wait/nop.go | 4 + .../testcontainers-go/wait/wait.go | 1 + vendor/modules.txt | 6 +- 42 files changed, 1527 insertions(+), 780 deletions(-) create mode 100644 vendor/github.com/testcontainers/testcontainers-go/.mockery.yaml create mode 100644 vendor/github.com/testcontainers/testcontainers-go/cleanup.go create mode 100644 vendor/github.com/testcontainers/testcontainers-go/generate.go delete mode 100644 vendor/github.com/testcontainers/testcontainers-go/testdata/.docker/config.json create mode 100644 vendor/github.com/testcontainers/testcontainers-go/wait/file.go diff --git a/go.mod b/go.mod index bce25978..d3d97b0b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/camunda-community-hub/zeebe-client-go/v8 -go 1.21 +go 1.22 + +toolchain go1.23.2 require ( github.com/docker/go-connections v0.5.0 @@ -11,7 +13,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 - github.com/testcontainers/testcontainers-go v0.33.0 + github.com/testcontainers/testcontainers-go v0.34.0 golang.org/x/net v0.29.0 golang.org/x/oauth2 v0.23.0 google.golang.org/grpc v1.67.0 @@ -28,7 +30,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.2.0+incompatible // indirect diff --git a/go.sum b/go.sum index 9820637a..de086fc8 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -120,6 +122,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= diff --git a/vendor/github.com/cpuguy83/dockercfg/auth.go b/vendor/github.com/cpuguy83/dockercfg/auth.go index 5a9891b8..106ab847 100644 --- a/vendor/github.com/cpuguy83/dockercfg/auth.go +++ b/vendor/github.com/cpuguy83/dockercfg/auth.go @@ -1,40 +1,43 @@ package dockercfg import ( + "bytes" "encoding/base64" "encoding/json" "errors" "fmt" - "os" + "io/fs" "os/exec" "runtime" "strings" ) -// This is used by the docker CLI in casses where an oauth identity token is used. -// In that case the username is stored litterally as `` -// When fetching the credentials we check for this value to determine if +// This is used by the docker CLI in cases where an oauth identity token is used. +// In that case the username is stored literally as `` +// When fetching the credentials we check for this value to determine if. const tokenUsername = "" // GetRegistryCredentials gets registry credentials for the passed in registry host. // -// This will use `LoadDefaultConfig` to read registry auth details from the config. +// This will use [LoadDefaultConfig] to read registry auth details from the config. // If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform. func GetRegistryCredentials(hostname string) (string, string, error) { cfg, err := LoadDefaultConfig() if err != nil { - if !os.IsNotExist(err) { - return "", "", err + if !errors.Is(err, fs.ErrNotExist) { + return "", "", fmt.Errorf("load default config: %w", err) } + return GetCredentialsFromHelper("", hostname) } + return cfg.GetRegistryCredentials(hostname) } // ResolveRegistryHost can be used to transform a docker registry host name into what is used for the docker config/cred helpers // // This is useful for using with containerd authorizers. -// Natrually this only transforms docker hub URLs. +// Naturally this only transforms docker hub URLs. func ResolveRegistryHost(host string) string { switch host { case "index.docker.io", "docker.io", "https://index.docker.io/v1/", "registry-1.docker.io": @@ -43,9 +46,9 @@ func ResolveRegistryHost(host string) string { return host } -// GetRegistryCredentials gets credentials, if any, for the provided hostname +// GetRegistryCredentials gets credentials, if any, for the provided hostname. // -// Hostnames should already be resolved using `ResolveRegistryAuth` +// Hostnames should already be resolved using [ResolveRegistryHost]. // // If the returned username string is empty, the password is an identity token. func (c *Config) GetRegistryCredentials(hostname string) (string, string, error) { @@ -55,7 +58,14 @@ func (c *Config) GetRegistryCredentials(hostname string) (string, string, error) } if c.CredentialsStore != "" { - return GetCredentialsFromHelper(c.CredentialsStore, hostname) + username, password, err := GetCredentialsFromHelper(c.CredentialsStore, hostname) + if err != nil { + return "", "", fmt.Errorf("get credentials from store: %w", err) + } + + if username != "" || password != "" { + return username, password, nil + } } auth, ok := c.AuthConfigs[hostname] @@ -87,78 +97,96 @@ func DecodeBase64Auth(auth AuthConfig) (string, string, error) { decoded := make([]byte, decLen) n, err := base64.StdEncoding.Decode(decoded, []byte(auth.Auth)) if err != nil { - return "", "", fmt.Errorf("error decoding auth from file: %w", err) + return "", "", fmt.Errorf("decode auth: %w", err) } - if n > decLen { - return "", "", fmt.Errorf("decoded value is longer than expected length, expected: %d, actual: %d", decLen, n) - } + decoded = decoded[:n] - split := strings.SplitN(string(decoded), ":", 2) - if len(split) != 2 { - return "", "", errors.New("invalid auth string") + const sep = ":" + user, pass, found := strings.Cut(string(decoded), sep) + if !found { + return "", "", fmt.Errorf("invalid auth: missing %q separator", sep) } - return split[0], strings.Trim(split[1], "\x00"), nil + return user, pass, nil } -// Errors from credential helpers +// Errors from credential helpers. var ( ErrCredentialsNotFound = errors.New("credentials not found in native keychain") ErrCredentialsMissingServerURL = errors.New("no credentials server URL") ) +//nolint:gochecknoglobals // These are used to mock exec in tests. +var ( + // execLookPath is a variable that can be used to mock exec.LookPath in tests. + execLookPath = exec.LookPath + // execCommand is a variable that can be used to mock exec.Command in tests. + execCommand = exec.Command +) + // GetCredentialsFromHelper attempts to lookup credentials from the passed in docker credential helper. // -// The credential helpoer should just be the suffix name (no "docker-credential-"). +// The credential helper should just be the suffix name (no "docker-credential-"). // If the passed in helper program is empty this will look up the default helper for the platform. // // If the credentials are not found, no error is returned, only empty credentials. // -// Hostnames should already be resolved using `ResolveRegistryAuth` +// Hostnames should already be resolved using [ResolveRegistryHost] // // If the username string is empty, the password string is an identity token. func GetCredentialsFromHelper(helper, hostname string) (string, string, error) { if helper == "" { - helper = getCredentialHelper() - } - if helper == "" { - return "", "", nil + helper, helperErr := getCredentialHelper() + if helperErr != nil { + return "", "", fmt.Errorf("get credential helper: %w", helperErr) + } + + if helper == "" { + return "", "", nil + } } - p, err := exec.LookPath("docker-credential-" + helper) + helper = "docker-credential-" + helper + p, err := execLookPath(helper) if err != nil { + if !errors.Is(err, exec.ErrNotFound) { + return "", "", fmt.Errorf("look up %q: %w", helper, err) + } + return "", "", nil } - cmd := exec.Command(p, "get") + var outBuf, errBuf bytes.Buffer + cmd := execCommand(p, "get") cmd.Stdin = strings.NewReader(hostname) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf - b, err := cmd.Output() - if err != nil { - s := strings.TrimSpace(string(b)) - - switch s { + if err = cmd.Run(); err != nil { + out := strings.TrimSpace(outBuf.String()) + switch out { case ErrCredentialsNotFound.Error(): return "", "", nil case ErrCredentialsMissingServerURL.Error(): - return "", "", errors.New(s) + return "", "", ErrCredentialsMissingServerURL default: + return "", "", fmt.Errorf("execute %q stdout: %q stderr: %q: %w", + helper, out, strings.TrimSpace(errBuf.String()), err, + ) } - - return "", "", err } var creds struct { - Username string - Secret string + Username string `json:"Username"` + Secret string `json:"Secret"` } - if err := json.Unmarshal(b, &creds); err != nil { - return "", "", err + if err = json.Unmarshal(outBuf.Bytes(), &creds); err != nil { + return "", "", fmt.Errorf("unmarshal credentials from: %q: %w", helper, err) } - // When tokenUsername is used, the output is an identity token and the username is garbage + // When tokenUsername is used, the output is an identity token and the username is garbage. if creds.Username == tokenUsername { creds.Username = "" } @@ -167,18 +195,21 @@ func GetCredentialsFromHelper(helper, hostname string) (string, string, error) { } // getCredentialHelper gets the default credential helper name for the current platform. -func getCredentialHelper() string { +func getCredentialHelper() (string, error) { switch runtime.GOOS { case "linux": - if _, err := exec.LookPath("pass"); err == nil { - return "pass" + if _, err := exec.LookPath("pass"); err != nil { + if errors.Is(err, exec.ErrNotFound) { + return "secretservice", nil + } + return "", fmt.Errorf(`look up "pass": %w`, err) } - return "secretservice" + return "pass", nil case "darwin": - return "osxkeychain" + return "osxkeychain", nil case "windows": - return "wincred" + return "wincred", nil default: - return "" + return "", nil } } diff --git a/vendor/github.com/cpuguy83/dockercfg/config.go b/vendor/github.com/cpuguy83/dockercfg/config.go index 88736cab..5e539079 100644 --- a/vendor/github.com/cpuguy83/dockercfg/config.go +++ b/vendor/github.com/cpuguy83/dockercfg/config.go @@ -13,7 +13,7 @@ type Config struct { DetachKeys string `json:"detachKeys,omitempty"` CredentialsStore string `json:"credsStore,omitempty"` CredentialHelpers map[string]string `json:"credHelpers,omitempty"` - Filename string `json:"-"` // Note: for internal use only + Filename string `json:"-"` // Note: for internal use only. ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` ServicesFormat string `json:"servicesFormat,omitempty"` TasksFormat string `json:"tasksFormat,omitempty"` @@ -30,7 +30,7 @@ type Config struct { Aliases map[string]string `json:"aliases,omitempty"` } -// ProxyConfig contains proxy configuration settings +// ProxyConfig contains proxy configuration settings. type ProxyConfig struct { HTTPProxy string `json:"httpProxy,omitempty"` HTTPSProxy string `json:"httpsProxy,omitempty"` @@ -38,7 +38,7 @@ type ProxyConfig struct { FTPProxy string `json:"ftpProxy,omitempty"` } -// AuthConfig contains authorization information for connecting to a Registry +// AuthConfig contains authorization information for connecting to a Registry. type AuthConfig struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` @@ -55,11 +55,11 @@ type AuthConfig struct { // an access token for the registry. IdentityToken string `json:"identitytoken,omitempty"` - // RegistryToken is a bearer token to be sent to a registry + // RegistryToken is a bearer token to be sent to a registry. RegistryToken string `json:"registrytoken,omitempty"` } -// KubernetesConfig contains Kubernetes orchestrator settings +// KubernetesConfig contains Kubernetes orchestrator settings. type KubernetesConfig struct { AllNamespaces string `json:"allNamespaces,omitempty"` } diff --git a/vendor/github.com/cpuguy83/dockercfg/load.go b/vendor/github.com/cpuguy83/dockercfg/load.go index 262545b9..a1c4dca0 100644 --- a/vendor/github.com/cpuguy83/dockercfg/load.go +++ b/vendor/github.com/cpuguy83/dockercfg/load.go @@ -11,7 +11,7 @@ import ( func UserHomeConfigPath() (string, error) { home, err := os.UserHomeDir() if err != nil { - return "", fmt.Errorf("error looking up user home dir: %w", err) + return "", fmt.Errorf("user home dir: %w", err) } return filepath.Join(home, ".docker", "config.json"), nil @@ -19,7 +19,7 @@ func UserHomeConfigPath() (string, error) { // ConfigPath returns the path to the docker cli config. // -// It will either use the DOCKER_CONFIG env var if set, or the value from `UserHomeConfigPath` +// It will either use the DOCKER_CONFIG env var if set, or the value from [UserHomeConfigPath] // DOCKER_CONFIG would be the dir path where `config.json` is stored, this returns the path to config.json. func ConfigPath() (string, error) { if p := os.Getenv("DOCKER_CONFIG"); p != "" { @@ -28,24 +28,28 @@ func ConfigPath() (string, error) { return UserHomeConfigPath() } -// LoadDefaultConfig loads the docker cli config from the path returned from `ConfigPath` +// LoadDefaultConfig loads the docker cli config from the path returned from [ConfigPath]. func LoadDefaultConfig() (Config, error) { var cfg Config p, err := ConfigPath() if err != nil { - return cfg, err + return cfg, fmt.Errorf("config path: %w", err) } + return cfg, FromFile(p, &cfg) } -// FromFile loads config from the specified path into cfg +// FromFile loads config from the specified path into cfg. func FromFile(configPath string, cfg *Config) error { f, err := os.Open(configPath) if err != nil { - return err + return fmt.Errorf("open config: %w", err) + } + defer f.Close() + + if err = json.NewDecoder(f).Decode(&cfg); err != nil { + return fmt.Errorf("decode config: %w", err) } - err = json.NewDecoder(f).Decode(&cfg) - f.Close() - return err + return nil } diff --git a/vendor/github.com/testcontainers/testcontainers-go/.gitignore b/vendor/github.com/testcontainers/testcontainers-go/.gitignore index 4b420b86..e5293563 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/.gitignore +++ b/vendor/github.com/testcontainers/testcontainers-go/.gitignore @@ -14,4 +14,7 @@ TEST-*.xml tcvenv -**/go.work \ No newline at end of file +**/go.work + +# VS Code settings +.vscode diff --git a/vendor/github.com/testcontainers/testcontainers-go/.golangci.yml b/vendor/github.com/testcontainers/testcontainers-go/.golangci.yml index 1791b9ca..26f8f8a3 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/.golangci.yml +++ b/vendor/github.com/testcontainers/testcontainers-go/.golangci.yml @@ -1,13 +1,15 @@ linters: enable: + - errcheck - errorlint - gci - gocritic - gofumpt - misspell + - nolintlint - nonamedreturns - testifylint - - errcheck + - thelper linters-settings: errorlint: @@ -29,16 +31,6 @@ linters-settings: disable: - float-compare - go-require - enable: - - bool-compare - - compares - - empty - - error-is-as - - error-nil - - expected-actual - - len - - require-error - - suite-dont-use-pkg - - suite-extra-assert-call + enable-all: true run: timeout: 5m diff --git a/vendor/github.com/testcontainers/testcontainers-go/.mockery.yaml b/vendor/github.com/testcontainers/testcontainers-go/.mockery.yaml new file mode 100644 index 00000000..2f96829f --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/.mockery.yaml @@ -0,0 +1,11 @@ +quiet: True +disable-version-string: True +with-expecter: True +mockname: "mock{{.InterfaceName}}" +filename: "{{ .InterfaceName | lower }}_mock_test.go" +outpkg: "{{.PackageName}}_test" +dir: "{{.InterfaceDir}}" +packages: + github.com/testcontainers/testcontainers-go/wait: + interfaces: + StrategyTarget: diff --git a/vendor/github.com/testcontainers/testcontainers-go/Pipfile b/vendor/github.com/testcontainers/testcontainers-go/Pipfile index 6d49cae4..26482787 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/Pipfile +++ b/vendor/github.com/testcontainers/testcontainers-go/Pipfile @@ -8,9 +8,9 @@ verify_ssl = true [packages] mkdocs = "==1.5.3" mkdocs-codeinclude-plugin = "==0.2.1" -mkdocs-include-markdown-plugin = "==6.2.1" +mkdocs-include-markdown-plugin = "==6.2.2" mkdocs-material = "==9.5.18" -mkdocs-markdownextradata-plugin = "==0.2.5" +mkdocs-markdownextradata-plugin = "==0.2.6" [requires] python_version = "3.8" diff --git a/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock b/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock index 3a6f97e0..9a2f6d24 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock +++ b/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "de89df66a55ec8bbae341b1e70d15bb1a52b1b04489eee82b2195c83ef08a385" + "sha256": "0411eac13d1b06b42671b8a654fb269eb0c329d9a3d41f669ccf7b653ef8ad32" }, "pipfile-spec": 6, "requires": { @@ -26,11 +26,11 @@ }, "bracex": { "hashes": [ - "sha256:a27eaf1df42cf561fed58b7a8f3fdf129d1ea16a81e1fadd1d17989bc6384beb", - "sha256:efdc71eff95eaff5e0f8cfebe7d01adf2c8637c8c92edaf63ef348c241a82418" + "sha256:0725da5045e8d37ea9592ab3614d8b561e22c3c5fde3964699be672e072ab611", + "sha256:d2fcf4b606a82ac325471affe1706dd9bbaa3536c91ef86a31f6b766f3dad1d0" ], "markers": "python_version >= '3.8'", - "version": "==2.4" + "version": "==2.5" }, "certifi": { "hashes": [ @@ -170,11 +170,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f", - "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812" + "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", + "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], "markers": "python_version < '3.10'", - "version": "==8.0.0" + "version": "==8.4.0" }, "jinja2": { "hashes": [ @@ -186,11 +186,11 @@ }, "markdown": { "hashes": [ - "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f", - "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224" + "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", + "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803" ], "markers": "python_version >= '3.8'", - "version": "==3.6" + "version": "==3.7" }, "markupsafe": { "hashes": [ @@ -286,20 +286,21 @@ }, "mkdocs-include-markdown-plugin": { "hashes": [ - "sha256:46fc372886d48eec541d36138d1fe1db42afd08b976ef7c8d8d4ea6ee4d5d1e8", - "sha256:8dfc3aee9435679b094cbdff023239e91d86cf357c40b0e99c28036449661830" + "sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7", + "sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==6.2.1" + "version": "==6.2.2" }, "mkdocs-markdownextradata-plugin": { "hashes": [ - "sha256:9c562e8fe375647d5692d11dfe369a7bdd50302174d35995fce2aeca58036ec6" + "sha256:34dd40870781784c75809596b2d8d879da783815b075336d541de1f150c94242", + "sha256:4aed9b43b8bec65b02598387426ca4809099ea5f5aa78bf114f3296fd46686b5" ], "index": "pypi", - "markers": "python_version not in '3.0, 3.1, 3.2, 3.3' and python_full_version >= '2.7.9'", - "version": "==0.2.5" + "markers": "python_version >= '3.6'", + "version": "==0.2.6" }, "mkdocs-material": { "hashes": [ @@ -382,60 +383,62 @@ }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "pyyaml-env-tag": { "hashes": [ @@ -558,57 +561,60 @@ }, "watchdog": { "hashes": [ - "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7", - "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", - "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175", - "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", - "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", - "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", - "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", - "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", - "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7", - "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28", - "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235", - "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", - "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", - "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5", - "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709", - "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee", - "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", - "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd", - "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba", - "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db", - "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682", - "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35", - "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", - "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645", - "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", - "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193", - "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b", - "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", - "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b", - "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625", - "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", - "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5" + "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", + "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", + "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", + "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", + "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", + "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", + "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", + "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", + "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", + "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", + "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", + "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", + "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", + "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", + "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", + "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", + "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", + "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", + "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", + "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", + "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", + "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", + "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", + "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", + "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", + "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", + "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", + "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", + "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", + "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", + "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", + "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", + "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", + "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", + "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3" ], "markers": "python_version >= '3.8'", - "version": "==4.0.1" + "version": "==4.0.2" }, "wcmatch": { "hashes": [ - "sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478", - "sha256:a70222b86dea82fb382dd87b73278c10756c138bd6f8f714e2183128887b9eb2" + "sha256:567d66b11ad74384954c8af86f607857c3bdf93682349ad32066231abd556c92", + "sha256:af25922e2b6dbd1550fa37a4c8de7dd558d6c1bb330c641de9b907b9776cb3c4" ], "markers": "python_version >= '3.8'", - "version": "==8.5.2" + "version": "==9.0" }, "zipp": { "hashes": [ - "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", - "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" + "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", + "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" ], "markers": "python_version >= '3.8'", - "version": "==3.19.2" + "version": "==3.20.1" } }, "develop": {} diff --git a/vendor/github.com/testcontainers/testcontainers-go/README.md b/vendor/github.com/testcontainers/testcontainers-go/README.md index cf7e0fc2..ea21c638 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/README.md +++ b/vendor/github.com/testcontainers/testcontainers-go/README.md @@ -1,27 +1,14 @@ # Testcontainers -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=141451032&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs) - -**Builds** - [![Main pipeline](https://github.com/testcontainers/testcontainers-go/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/testcontainers/testcontainers-go/actions/workflows/ci.yml) - -**Documentation** - [![GoDoc Reference](https://pkg.go.dev/badge/github.com/testcontainers/testcontainers-go.svg)](https://pkg.go.dev/github.com/testcontainers/testcontainers-go) - -**Social** - -[![Slack](https://img.shields.io/badge/Slack-4A154B?logo=slack)](https://testcontainers.slack.com/) - -**Code quality** - [![Go Report Card](https://goreportcard.com/badge/github.com/testcontainers/testcontainers-go)](https://goreportcard.com/report/github.com/testcontainers/testcontainers-go) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=testcontainers_testcontainers-go&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=testcontainers_testcontainers-go) +[![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/testcontainers/testcontainers-go/blob/main/LICENSE) -**License** +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=141451032&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs) -[![License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/testcontainers/testcontainers-go/blob/main/LICENSE) +[![Join our Slack](https://img.shields.io/badge/Slack-4A154B?logo=slack)](https://testcontainers.slack.com/) _Testcontainers for Go_ is a Go package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers diff --git a/vendor/github.com/testcontainers/testcontainers-go/cleanup.go b/vendor/github.com/testcontainers/testcontainers-go/cleanup.go new file mode 100644 index 00000000..e2d52440 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/cleanup.go @@ -0,0 +1,107 @@ +package testcontainers + +import ( + "context" + "errors" + "fmt" + "reflect" + "time" +) + +// terminateOptions is a type that holds the options for terminating a container. +type terminateOptions struct { + ctx context.Context + timeout *time.Duration + volumes []string +} + +// TerminateOption is a type that represents an option for terminating a container. +type TerminateOption func(*terminateOptions) + +// StopContext returns a TerminateOption that sets the context. +// Default: context.Background(). +func StopContext(ctx context.Context) TerminateOption { + return func(c *terminateOptions) { + c.ctx = ctx + } +} + +// StopTimeout returns a TerminateOption that sets the timeout. +// Default: See [Container.Stop]. +func StopTimeout(timeout time.Duration) TerminateOption { + return func(c *terminateOptions) { + c.timeout = &timeout + } +} + +// RemoveVolumes returns a TerminateOption that sets additional volumes to remove. +// This is useful when the container creates named volumes that should be removed +// which are not removed by default. +// Default: nil. +func RemoveVolumes(volumes ...string) TerminateOption { + return func(c *terminateOptions) { + c.volumes = volumes + } +} + +// TerminateContainer calls [Container.Terminate] on the container if it is not nil. +// +// This should be called as a defer directly after [GenericContainer](...) +// or a modules Run(...) to ensure the container is terminated when the +// function ends. +func TerminateContainer(container Container, options ...TerminateOption) error { + if isNil(container) { + return nil + } + + c := &terminateOptions{ + ctx: context.Background(), + } + + for _, opt := range options { + opt(c) + } + + // TODO: Add a timeout when terminate supports it. + err := container.Terminate(c.ctx) + if !isCleanupSafe(err) { + return fmt.Errorf("terminate: %w", err) + } + + // Remove additional volumes if any. + if len(c.volumes) == 0 { + return nil + } + + client, err := NewDockerClientWithOpts(c.ctx) + if err != nil { + return fmt.Errorf("docker client: %w", err) + } + + defer client.Close() + + // Best effort to remove all volumes. + var errs []error + for _, volume := range c.volumes { + if errRemove := client.VolumeRemove(c.ctx, volume, true); errRemove != nil { + errs = append(errs, fmt.Errorf("volume remove %q: %w", volume, errRemove)) + } + } + + return errors.Join(errs...) +} + +// isNil returns true if val is nil or an nil instance false otherwise. +func isNil(val any) bool { + if val == nil { + return true + } + + valueOf := reflect.ValueOf(val) + switch valueOf.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return valueOf.IsNil() + default: + return false + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk b/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk index 04d0a6e7..d168ff5c 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk +++ b/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk @@ -6,18 +6,22 @@ define go_install endef $(GOBIN)/golangci-lint: - $(call go_install,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1) + $(call go_install,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61.0) $(GOBIN)/gotestsum: $(call go_install,gotest.tools/gotestsum@latest) +$(GOBIN)/mockery: + $(call go_install,github.com/vektra/mockery/v2@v2.45) + .PHONY: install -install: $(GOBIN)/golangci-lint $(GOBIN)/gotestsum +install: $(GOBIN)/golangci-lint $(GOBIN)/gotestsum $(GOBIN)/mockery .PHONY: clean clean: rm $(GOBIN)/golangci-lint rm $(GOBIN)/gotestsum + rm $(GOBIN)/mockery .PHONY: dependencies-scan dependencies-scan: @@ -26,7 +30,11 @@ dependencies-scan: .PHONY: lint lint: $(GOBIN)/golangci-lint - golangci-lint run --out-format=github-actions --path-prefix=. --verbose -c $(ROOT_DIR)/.golangci.yml --fix + golangci-lint run --out-format=colored-line-number --path-prefix=. --verbose -c $(ROOT_DIR)/.golangci.yml --fix + +.PHONY: generate +generate: $(GOBIN)/mockery + go generate ./... .PHONY: test-% test-%: $(GOBIN)/gotestsum @@ -51,3 +59,6 @@ test-tools: $(GOBIN)/gotestsum .PHONY: tidy tidy: go mod tidy + +.PHONY: pre-commit +pre-commit: generate tidy lint diff --git a/vendor/github.com/testcontainers/testcontainers-go/container.go b/vendor/github.com/testcontainers/testcontainers-go/container.go index 8747335a..d114a598 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/container.go +++ b/vendor/github.com/testcontainers/testcontainers-go/container.go @@ -37,17 +37,17 @@ type DeprecatedContainer interface { // Container allows getting info about and controlling a single container instance type Container interface { - GetContainerID() string // get the container id from the provider - Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port - PortEndpoint(context.Context, nat.Port, string) (string, error) // get proto://ip:port string for the given exposed port - Host(context.Context) (string, error) // get host where the container port is exposed - Inspect(context.Context) (*types.ContainerJSON, error) // get container info - MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port - Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead - SessionID() string // get session id - IsRunning() bool // IsRunning returns true if the container is running, false otherwise. - Start(context.Context) error // start the container - Stop(context.Context, *time.Duration) error // stop the container + GetContainerID() string // get the container id from the provider + Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port + PortEndpoint(ctx context.Context, port nat.Port, proto string) (string, error) // get proto://ip:port string for the given exposed port + Host(context.Context) (string, error) // get host where the container port is exposed + Inspect(context.Context) (*types.ContainerJSON, error) // get container info + MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port + Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead + SessionID() string // get session id + IsRunning() bool // IsRunning returns true if the container is running, false otherwise. + Start(context.Context) error // start the container + Stop(context.Context, *time.Duration) error // stop the container // Terminate stops and removes the container and its image if it was built and not flagged as kept. Terminate(ctx context.Context) error @@ -460,7 +460,14 @@ func (c *ContainerRequest) BuildOptions() (types.ImageBuildOptions, error) { } if !c.ShouldKeepBuiltImage() { - buildOptions.Labels = core.DefaultLabels(core.SessionID()) + dst := GenericLabels() + if err = core.MergeCustomLabels(dst, c.Labels); err != nil { + return types.ImageBuildOptions{}, err + } + if err = core.MergeCustomLabels(dst, buildOptions.Labels); err != nil { + return types.ImageBuildOptions{}, err + } + buildOptions.Labels = dst } // Do this as late as possible to ensure we don't leak the context on error/panic. @@ -513,7 +520,7 @@ func (c *ContainerRequest) validateMounts() error { c.HostConfigModifier(&hostConfig) - if hostConfig.Binds != nil && len(hostConfig.Binds) > 0 { + if len(hostConfig.Binds) > 0 { for _, bind := range hostConfig.Binds { parts := strings.Split(bind, ":") if len(parts) != 2 { diff --git a/vendor/github.com/testcontainers/testcontainers-go/docker.go b/vendor/github.com/testcontainers/testcontainers-go/docker.go index 5f6c4156..2ef8c697 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/docker.go +++ b/vendor/github.com/testcontainers/testcontainers-go/docker.go @@ -5,7 +5,6 @@ import ( "bufio" "context" "encoding/base64" - "encoding/binary" "encoding/json" "errors" "fmt" @@ -17,7 +16,6 @@ import ( "path/filepath" "regexp" "strings" - "sync" "time" "github.com/cenkalti/backoff/v4" @@ -30,6 +28,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" "github.com/moby/term" specs "github.com/opencontainers/image-spec/specs-go/v1" @@ -48,11 +47,21 @@ const ( Podman = "podman" ReaperDefault = "reaper_default" // Default network name when bridge is not available packagePath = "github.com/testcontainers/testcontainers-go" - - logStoppedForOutOfSyncMessage = "Stopping log consumer: Headers out of sync" ) -var createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") +var ( + // createContainerFailDueToNameConflictRegex is a regular expression that matches the container is already in use error. + createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") + + // minLogProductionTimeout is the minimum log production timeout. + minLogProductionTimeout = time.Duration(5 * time.Second) + + // maxLogProductionTimeout is the maximum log production timeout. + maxLogProductionTimeout = time.Duration(60 * time.Second) + + // errLogProductionStop is the cause for stopping log production. + errLogProductionStop = errors.New("log production stopped") +) // DockerContainer represents a container started using Docker type DockerContainer struct { @@ -65,23 +74,19 @@ type DockerContainer struct { isRunning bool imageWasBuilt bool // keepBuiltImage makes Terminate not remove the image if imageWasBuilt. - keepBuiltImage bool - provider *DockerProvider - sessionID string - terminationSignal chan bool - consumers []LogConsumer - logProductionError chan error + keepBuiltImage bool + provider *DockerProvider + sessionID string + terminationSignal chan bool + consumers []LogConsumer // TODO: Remove locking and wait group once the deprecated StartLogProducer and // StopLogProducer have been removed and hence logging can only be started and // stopped once. - // logProductionWaitGroup is used to signal when the log production has stopped. - // This allows stopLogProduction to safely set logProductionStop to nil. - // See simplification in https://go.dev/play/p/x0pOElF2Vjf - logProductionWaitGroup sync.WaitGroup - - logProductionStop chan struct{} + // logProductionCancel is used to signal the log production to stop. + logProductionCancel context.CancelCauseFunc + logProductionCtx context.Context logProductionTimeout *time.Duration logger Logging @@ -178,7 +183,7 @@ func (c *DockerContainer) Inspect(ctx context.Context) (*types.ContainerJSON, er func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) { inspect, err := c.Inspect(ctx) if err != nil { - return "", err + return "", fmt.Errorf("inspect: %w", err) } if inspect.ContainerJSONBase.HostConfig.NetworkMode == "host" { return port, nil @@ -199,7 +204,7 @@ func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Po return nat.NewPort(k.Proto(), p[0].HostPort) } - return "", errors.New("port not found") + return "", errdefs.NotFound(fmt.Errorf("port %q not found", port)) } // Deprecated: use c.Inspect(ctx).NetworkSettings.Ports instead. @@ -259,9 +264,13 @@ func (c *DockerContainer) Start(ctx context.Context) error { // // If the container is already stopped, the method is a no-op. func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) error { + // Note we can't check isRunning here because we allow external creation + // without exposing the ability to fully initialize the container state. + // See: https://github.com/testcontainers/testcontainers-go/issues/2667 + // TODO: Add a check for isRunning when the above issue is resolved. err := c.stoppingHook(ctx) if err != nil { - return err + return fmt.Errorf("stopping hook: %w", err) } var options container.StopOptions @@ -272,30 +281,48 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro } if err := c.provider.client.ContainerStop(ctx, c.ID, options); err != nil { - return err + return fmt.Errorf("container stop: %w", err) } + defer c.provider.Close() c.isRunning = false err = c.stoppedHook(ctx) if err != nil { - return err + return fmt.Errorf("stopped hook: %w", err) } return nil } -// Terminate is used to kill the container. It is usually triggered by as defer function. +// Terminate calls stops and then removes the container including its volumes. +// If its image was built it and all child images are also removed unless +// the [FromDockerfile.KeepImage] on the [ContainerRequest] was set to true. +// +// The following hooks are called in order: +// - [ContainerLifecycleHooks.PreTerminates] +// - [ContainerLifecycleHooks.PostTerminates] func (c *DockerContainer) Terminate(ctx context.Context) error { + // ContainerRemove hardcodes stop timeout to 3 seconds which is too short + // to ensure that child containers are stopped so we manually call stop. + // TODO: make this configurable via a functional option. + timeout := 10 * time.Second + err := c.Stop(ctx, &timeout) + if err != nil && !isCleanupSafe(err) { + return fmt.Errorf("stop: %w", err) + } + select { - // close reaper if it was created + // Close reaper connection if it was attached. case c.terminationSignal <- true: default: } defer c.provider.client.Close() + // TODO: Handle errors from ContainerRemove more correctly, e.g. should we + // run the terminated hook? errs := []error{ c.terminatingHook(ctx), c.provider.client.ContainerRemove(ctx, c.GetContainerID(), container.RemoveOptions{ @@ -667,6 +694,29 @@ func (c *DockerContainer) copyToContainer(ctx context.Context, fileContent func( return nil } +// logConsumerWriter is a writer that writes to a LogConsumer. +type logConsumerWriter struct { + log Log + consumers []LogConsumer +} + +// newLogConsumerWriter creates a new logConsumerWriter for logType that sends messages to all consumers. +func newLogConsumerWriter(logType string, consumers []LogConsumer) *logConsumerWriter { + return &logConsumerWriter{ + log: Log{LogType: logType}, + consumers: consumers, + } +} + +// Write writes the p content to all consumers. +func (lw logConsumerWriter) Write(p []byte) (int, error) { + lw.log.Content = p + for _, consumer := range lw.consumers { + consumer.Accept(lw.log) + } + return len(p), nil +} + type LogProductionOption func(*DockerContainer) // WithLogProductionTimeout is a functional option that sets the timeout for the log production. @@ -684,124 +734,94 @@ func (c *DockerContainer) StartLogProducer(ctx context.Context, opts ...LogProdu // startLogProduction will start a concurrent process that will continuously read logs // from the container and will send them to each added LogConsumer. +// // Default log production timeout is 5s. It is used to set the context timeout -// which means that each log-reading loop will last at least the specified timeout -// and that it cannot be cancelled earlier. +// which means that each log-reading loop will last at up to the specified timeout. +// // Use functional option WithLogProductionTimeout() to override default timeout. If it's // lower than 5s and greater than 60s it will be set to 5s or 60s respectively. func (c *DockerContainer) startLogProduction(ctx context.Context, opts ...LogProductionOption) error { - c.logProductionStop = make(chan struct{}, 1) // buffered channel to avoid blocking - c.logProductionWaitGroup.Add(1) - for _, opt := range opts { opt(c) } - minLogProductionTimeout := time.Duration(5 * time.Second) - maxLogProductionTimeout := time.Duration(60 * time.Second) - - if c.logProductionTimeout == nil { + // Validate the log production timeout. + switch { + case c.logProductionTimeout == nil: c.logProductionTimeout = &minLogProductionTimeout - } - - if *c.logProductionTimeout < minLogProductionTimeout { + case *c.logProductionTimeout < minLogProductionTimeout: c.logProductionTimeout = &minLogProductionTimeout - } - - if *c.logProductionTimeout > maxLogProductionTimeout { + case *c.logProductionTimeout > maxLogProductionTimeout: c.logProductionTimeout = &maxLogProductionTimeout } - c.logProductionError = make(chan error, 1) + // Setup the log writers. + stdout := newLogConsumerWriter(StdoutLog, c.consumers) + stderr := newLogConsumerWriter(StderrLog, c.consumers) + + // Setup the log production context which will be used to stop the log production. + c.logProductionCtx, c.logProductionCancel = context.WithCancelCause(ctx) go func() { - defer func() { - close(c.logProductionError) - c.logProductionWaitGroup.Done() - }() + err := c.logProducer(stdout, stderr) + // Set context cancel cause, if not already set. + c.logProductionCancel(err) + }() - since := "" - // if the socket is closed we will make additional logs request with updated Since timestamp - BEGIN: - options := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - Follow: true, - Since: since, - } + return nil +} + +// logProducer read logs from the container and writes them to stdout, stderr until either: +// - logProductionCtx is done +// - A fatal error occurs +// - No more logs are available +func (c *DockerContainer) logProducer(stdout, stderr io.Writer) error { + // Clean up idle client connections. + defer c.provider.Close() + + // Setup the log options, start from the beginning. + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + } - ctx, cancel := context.WithTimeout(ctx, *c.logProductionTimeout) + for { + timeoutCtx, cancel := context.WithTimeout(c.logProductionCtx, *c.logProductionTimeout) defer cancel() - r, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options) - if err != nil { - c.logProductionError <- err - return + err := c.copyLogs(timeoutCtx, stdout, stderr, options) + switch { + case err == nil: + // No more logs available. + return nil + case c.logProductionCtx.Err() != nil: + // Log production was stopped or caller context is done. + return nil + case timeoutCtx.Err() != nil, errors.Is(err, net.ErrClosed): + // Timeout or client connection closed, retry. + default: + // Unexpected error, retry. + Logger.Printf("Unexpected error reading logs: %v", err) } - defer c.provider.Close() - for { - select { - case <-c.logProductionStop: - c.logProductionError <- r.Close() - return - default: - } - h := make([]byte, 8) - _, err := io.ReadFull(r, h) - if err != nil { - switch { - case err == io.EOF: - // No more logs coming - case errors.Is(err, net.ErrClosed): - now := time.Now() - since = fmt.Sprintf("%d.%09d", now.Unix(), int64(now.Nanosecond())) - goto BEGIN - case errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled): - // Probably safe to continue here - continue - default: - _, _ = fmt.Fprintf(os.Stderr, "container log error: %+v. %s", err, logStoppedForOutOfSyncMessage) - // if we would continue here, the next header-read will result into random data... - } - return - } - - count := binary.BigEndian.Uint32(h[4:]) - if count == 0 { - continue - } - logType := h[0] - if logType > 2 { - _, _ = fmt.Fprintf(os.Stderr, "received invalid log type: %d", logType) - // sometimes docker returns logType = 3 which is an undocumented log type, so treat it as stdout - logType = 1 - } + // Retry from the last log received. + now := time.Now() + options.Since = fmt.Sprintf("%d.%09d", now.Unix(), int64(now.Nanosecond())) + } +} - // a map of the log type --> int representation in the header, notice the first is blank, this is stdin, but the go docker client doesn't allow following that in logs - logTypes := []string{"", StdoutLog, StderrLog} +// copyLogs copies logs from the container to stdout and stderr. +func (c *DockerContainer) copyLogs(ctx context.Context, stdout, stderr io.Writer, options container.LogsOptions) error { + rc, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options) + if err != nil { + return fmt.Errorf("container logs: %w", err) + } + defer rc.Close() - b := make([]byte, count) - _, err = io.ReadFull(r, b) - if err != nil { - // TODO: add-logger: use logger to log out this error - _, _ = fmt.Fprintf(os.Stderr, "error occurred reading log with known length %s", err.Error()) - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - // Probably safe to continue here - continue - } - // we can not continue here as the next read most likely will not be the next header - _, _ = fmt.Fprintln(os.Stderr, logStoppedForOutOfSyncMessage) - return - } - for _, c := range c.consumers { - c.Accept(Log{ - LogType: logTypes[logType], - Content: b, - }) - } - } - }() + if _, err = stdcopy.StdCopy(stdout, stderr, rc); err != nil { + return fmt.Errorf("stdcopy: %w", err) + } return nil } @@ -814,18 +834,25 @@ func (c *DockerContainer) StopLogProducer() error { // stopLogProduction will stop the concurrent process that is reading logs // and sending them to each added LogConsumer func (c *DockerContainer) stopLogProduction() error { - // signal the log production to stop - c.logProductionStop <- struct{}{} + if c.logProductionCancel == nil { + return nil + } - c.logProductionWaitGroup.Wait() + // Signal the log production to stop. + c.logProductionCancel(errLogProductionStop) - if err := <-c.logProductionError; err != nil { - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - // Returning context errors is not useful for the consumer. + if err := context.Cause(c.logProductionCtx); err != nil { + switch { + case errors.Is(err, errLogProductionStop): + // Log production was stopped. + return nil + case errors.Is(err, context.DeadlineExceeded), + errors.Is(err, context.Canceled): + // Parent context is done. return nil + default: + return err } - - return err } return nil @@ -834,7 +861,16 @@ func (c *DockerContainer) stopLogProduction() error { // GetLogProductionErrorChannel exposes the only way for the consumer // to be able to listen to errors and react to them. func (c *DockerContainer) GetLogProductionErrorChannel() <-chan error { - return c.logProductionError + if c.logProductionCtx == nil { + return nil + } + + errCh := make(chan error, 1) + go func() { + <-c.logProductionCtx.Done() + errCh <- context.Cause(c.logProductionCtx) + }() + return errCh } // DockerNetwork represents a network started using Docker @@ -944,9 +980,7 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st } // CreateContainer fulfils a request for a container without starting it -func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { - var err error - +func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (con Container, err error) { //nolint:nonamedreturns // Needed for error checking. // defer the close of the Docker client connection the soonest defer p.Close() @@ -991,22 +1025,23 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque // the reaper does not need to start a reaper for itself isReaperContainer := strings.HasSuffix(imageName, config.ReaperDefaultImage) if !p.config.RyukDisabled && !isReaperContainer { - r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), core.SessionID(), p) + r, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), core.SessionID(), p) if err != nil { - return nil, fmt.Errorf("%w: creating reaper failed", err) + return nil, fmt.Errorf("reaper: %w", err) } - termSignal, err = r.Connect() + + termSignal, err := r.Connect() if err != nil { - return nil, fmt.Errorf("%w: connecting to reaper failed", err) + return nil, fmt.Errorf("reaper connect: %w", err) } - } - // Cleanup on error, otherwise set termSignal to nil before successful return. - defer func() { - if termSignal != nil { - termSignal <- true - } - }() + // Cleanup on error. + defer func() { + if err != nil { + termSignal <- true + } + }() + } if err = req.Validate(); err != nil { return nil, err @@ -1072,10 +1107,9 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque } if !isReaperContainer { - // add the labels that the reaper will use to terminate the container to the request - for k, v := range core.DefaultLabels(core.SessionID()) { - req.Labels[k] = v - } + // Add the labels that identify this as a testcontainers container and + // allow the reaper to terminate it if requested. + AddGenericLabels(req.Labels) } dockerInput := &container.Config{ @@ -1169,9 +1203,6 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque return nil, err } - // Disable cleanup on success - termSignal = nil - return c, nil } @@ -1220,7 +1251,7 @@ func (p *DockerProvider) waitContainerCreation(ctx context.Context, name string) ) } -func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { +func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (con Container, err error) { //nolint:nonamedreturns // Needed for error check. c, err := p.findContainerByName(ctx, req.Name) if err != nil { return nil, err @@ -1243,14 +1274,22 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain var termSignal chan bool if !p.config.RyukDisabled { - r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) + r, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) if err != nil { return nil, fmt.Errorf("reaper: %w", err) } - termSignal, err = r.Connect() + + termSignal, err := r.Connect() if err != nil { - return nil, fmt.Errorf("%w: connecting to reaper failed", err) + return nil, fmt.Errorf("reaper connect: %w", err) } + + // Cleanup on error. + defer func() { + if err != nil { + termSignal <- true + } + }() } // default hooks include logger hook and pre-create hook @@ -1418,9 +1457,7 @@ func daemonHost(ctx context.Context, p *DockerProvider) (string, error) { // Deprecated: use network.New instead // CreateNetwork returns the object representing a new network identified by its name -func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (Network, error) { - var err error - +func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) (net Network, err error) { //nolint:nonamedreturns // Needed for error check. // defer the close of the Docker client connection the soonest defer p.Close() @@ -1449,31 +1486,30 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) var termSignal chan bool if !p.config.RyukDisabled { - r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) + r, err := spawner.reaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p) if err != nil { - return nil, fmt.Errorf("%w: creating network reaper failed", err) + return nil, fmt.Errorf("reaper: %w", err) } - termSignal, err = r.Connect() + + termSignal, err := r.Connect() if err != nil { - return nil, fmt.Errorf("%w: connecting to network reaper failed", err) + return nil, fmt.Errorf("reaper connect: %w", err) } - } - // add the labels that the reaper will use to terminate the network to the request - for k, v := range core.DefaultLabels(sessionID) { - req.Labels[k] = v + // Cleanup on error. + defer func() { + if err != nil { + termSignal <- true + } + }() } - // Cleanup on error, otherwise set termSignal to nil before successful return. - defer func() { - if termSignal != nil { - termSignal <- true - } - }() + // add the labels that the reaper will use to terminate the network to the request + core.AddDefaultLabels(sessionID, req.Labels) response, err := p.client.NetworkCreate(ctx, req.Name, nc) if err != nil { - return &DockerNetwork{}, err + return &DockerNetwork{}, fmt.Errorf("create network: %w", err) } n := &DockerNetwork{ @@ -1484,9 +1520,6 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest) provider: p, } - // Disable cleanup on success - termSignal = nil - return n, nil } @@ -1556,9 +1589,12 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl _, err = cli.NetworkCreate(ctx, reaperNetwork, network.CreateOptions{ Driver: Bridge, Attachable: true, - Labels: core.DefaultLabels(core.SessionID()), + Labels: GenericLabels(), }) - if err != nil { + // If the network already exists, we can ignore the error as that can + // happen if we are running multiple tests in parallel and we only + // need to ensure that the network exists. + if err != nil && !errdefs.IsConflict(err) { return "", err } } @@ -1596,7 +1632,7 @@ func containerFromDockerResponse(ctx context.Context, response types.Container) // populate the raw representation of the container jsonRaw, err := ctr.inspectRawContainer(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("inspect raw container: %w", err) } // the health status of the container, if any diff --git a/vendor/github.com/testcontainers/testcontainers-go/docker_auth.go b/vendor/github.com/testcontainers/testcontainers-go/docker_auth.go index 99e2d2fd..af0d415d 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/docker_auth.go +++ b/vendor/github.com/testcontainers/testcontainers-go/docker_auth.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/url" "os" "sync" @@ -137,24 +136,12 @@ func (c *credentialsCache) Get(hostname, configKey string) (string, string, erro return user, password, nil } -// configFileKey returns a key to use for caching credentials based on +// configKey returns a key to use for caching credentials based on // the contents of the currently active config. -func configFileKey() (string, error) { - configPath, err := dockercfg.ConfigPath() - if err != nil { - return "", err - } - - f, err := os.Open(configPath) - if err != nil { - return "", fmt.Errorf("open config file: %w", err) - } - - defer f.Close() - +func configKey(cfg *dockercfg.Config) (string, error) { h := md5.New() - if _, err := io.Copy(h, f); err != nil { - return "", fmt.Errorf("copying config file: %w", err) + if err := json.NewEncoder(h).Encode(cfg); err != nil { + return "", fmt.Errorf("encode config: %w", err) } return hex.EncodeToString(h.Sum(nil)), nil @@ -165,10 +152,14 @@ func configFileKey() (string, error) { func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { cfg, err := getDockerConfig() if err != nil { + if errors.Is(err, os.ErrNotExist) { + return map[string]registry.AuthConfig{}, nil + } + return nil, err } - configKey, err := configFileKey() + key, err := configKey(cfg) if err != nil { return nil, err } @@ -195,7 +186,7 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { switch { case ac.Username == "" && ac.Password == "": // Look up credentials from the credential store. - u, p, err := creds.Get(k, configKey) + u, p, err := creds.Get(k, key) if err != nil { results <- authConfigResult{err: err} return @@ -218,7 +209,7 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { go func(k string) { defer wg.Done() - u, p, err := creds.Get(k, configKey) + u, p, err := creds.Get(k, key) if err != nil { results <- authConfigResult{err: err} return @@ -260,20 +251,20 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { // 1. the DOCKER_AUTH_CONFIG environment variable, unmarshalling it into a dockercfg.Config // 2. the DOCKER_CONFIG environment variable, as the path to the config file // 3. else it will load the default config file, which is ~/.docker/config.json -func getDockerConfig() (dockercfg.Config, error) { - dockerAuthConfig := os.Getenv("DOCKER_AUTH_CONFIG") - if dockerAuthConfig != "" { - cfg := dockercfg.Config{} - err := json.Unmarshal([]byte(dockerAuthConfig), &cfg) - if err == nil { - return cfg, nil +func getDockerConfig() (*dockercfg.Config, error) { + if env := os.Getenv("DOCKER_AUTH_CONFIG"); env != "" { + var cfg dockercfg.Config + if err := json.Unmarshal([]byte(env), &cfg); err != nil { + return nil, fmt.Errorf("unmarshal DOCKER_AUTH_CONFIG: %w", err) } + + return &cfg, nil } cfg, err := dockercfg.LoadDefaultConfig() if err != nil { - return cfg, err + return nil, fmt.Errorf("load default config: %w", err) } - return cfg, nil + return &cfg, nil } diff --git a/vendor/github.com/testcontainers/testcontainers-go/docker_client.go b/vendor/github.com/testcontainers/testcontainers-go/docker_client.go index c8e8e825..04df7129 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/docker_client.go +++ b/vendor/github.com/testcontainers/testcontainers-go/docker_client.go @@ -79,8 +79,8 @@ func (c *DockerClient) Info(ctx context.Context) (system.Info, error) { dockerInfo.OperatingSystem, dockerInfo.MemTotal/1024/1024, infoLabels, internal.Version, - core.ExtractDockerHost(ctx), - core.ExtractDockerSocket(ctx), + core.MustExtractDockerHost(ctx), + core.MustExtractDockerSocket(ctx), core.SessionID(), core.ProcessID(), ) diff --git a/vendor/github.com/testcontainers/testcontainers-go/docker_mounts.go b/vendor/github.com/testcontainers/testcontainers-go/docker_mounts.go index aed30103..d8af3fae 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/docker_mounts.go +++ b/vendor/github.com/testcontainers/testcontainers-go/docker_mounts.go @@ -126,9 +126,7 @@ func mapToDockerMounts(containerMounts ContainerMounts) []mount.Mount { Labels: make(map[string]string), } } - for k, v := range GenericLabels() { - containerMount.VolumeOptions.Labels[k] = v - } + AddGenericLabels(containerMount.VolumeOptions.Labels) } mounts = append(mounts, containerMount) diff --git a/vendor/github.com/testcontainers/testcontainers-go/generate.go b/vendor/github.com/testcontainers/testcontainers-go/generate.go new file mode 100644 index 00000000..19ae4969 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/generate.go @@ -0,0 +1,3 @@ +package testcontainers + +//go:generate mockery diff --git a/vendor/github.com/testcontainers/testcontainers-go/generic.go b/vendor/github.com/testcontainers/testcontainers-go/generic.go index 4c214744..fd13a607 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/generic.go +++ b/vendor/github.com/testcontainers/testcontainers-go/generic.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "sync" "github.com/testcontainers/testcontainers-go/internal/core" @@ -74,6 +75,14 @@ func GenericContainer(ctx context.Context, req GenericContainerRequest) (Contain } if err != nil { // At this point `c` might not be nil. Give the caller an opportunity to call Destroy on the container. + // TODO: Remove this debugging. + if strings.Contains(err.Error(), "toomanyrequests") { + // Debugging information for rate limiting. + cfg, err := getDockerConfig() + if err == nil { + fmt.Printf("XXX: too many requests: %+v", cfg) + } + } return c, fmt.Errorf("create container: %w", err) } @@ -92,7 +101,17 @@ type GenericProvider interface { ImageProvider } -// GenericLabels returns a map of labels that can be used to identify containers created by this library +// GenericLabels returns a map of labels that can be used to identify resources +// created by this library. This includes the standard LabelSessionID if the +// reaper is enabled, otherwise this is excluded to prevent resources being +// incorrectly reaped. func GenericLabels() map[string]string { return core.DefaultLabels(core.SessionID()) } + +// AddGenericLabels adds the generic labels to target. +func AddGenericLabels(target map[string]string) { + for k, v := range GenericLabels() { + target[k] = v + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go b/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go index f1d702ec..b0bcc24d 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go @@ -11,7 +11,7 @@ import ( "github.com/magiconair/properties" ) -const ReaperDefaultImage = "testcontainers/ryuk:0.8.1" +const ReaperDefaultImage = "testcontainers/ryuk:0.10.2" var ( tcConfig Config diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go index 64af509b..04a54bcb 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/client.go @@ -14,7 +14,7 @@ import ( func NewClient(ctx context.Context, ops ...client.Opt) (*client.Client, error) { tcConfig := config.Read() - dockerHost := ExtractDockerHost(ctx) + dockerHost := MustExtractDockerHost(ctx) opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()} if dockerHost != "" { 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 index b9d2d60d..3088a374 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_host.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_host.go @@ -56,7 +56,24 @@ func DefaultGatewayIP() (string, error) { return ip, nil } -// ExtractDockerHost Extracts the docker host from the different alternatives, caching the result to avoid unnecessary +// 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: // @@ -66,16 +83,21 @@ func DefaultGatewayIP() (string, error) { // 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 { +// 7. Else, because the Docker host is not set, it panics. +func MustExtractDockerHost(ctx context.Context) string { dockerHostOnce.Do(func() { - dockerHostCache = extractDockerHost(ctx) + cache, err := extractDockerHost(ctx) + if err != nil { + panic(err) + } + + dockerHostCache = cache }) return dockerHostCache } -// ExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema and +// 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: @@ -83,12 +105,12 @@ func ExtractDockerHost(ctx context.Context) string { // 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. +// 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) // -// In any case, if the docker socket schema is "tcp://", the default docker socket path will be returned. -func ExtractDockerSocket(ctx context.Context) string { +// 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) }) @@ -98,7 +120,7 @@ func ExtractDockerSocket(ctx context.Context) string { // 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 { +func extractDockerHost(ctx context.Context) (string, error) { dockerHostFns := []func(context.Context) (string, error){ testcontainersHostFromProperties, dockerHostFromEnv, @@ -108,25 +130,35 @@ func extractDockerHost(ctx context.Context) string { rootlessDockerSocketPath, } - outerErr := ErrSocketNotFound + var errs []error for _, dockerHostFn := range dockerHostFns { dockerHost, err := dockerHostFn(ctx) if err != nil { - outerErr = fmt.Errorf("%w: %w", outerErr, err) + if !isHostNotSet(err) { + errs = append(errs, err) + } continue } - return dockerHost + if err = dockerHostCheck(ctx, dockerHost); err != nil { + errs = append(errs, fmt.Errorf("check host %q: %w", dockerHost, err)) + continue + } + + return dockerHost, nil } - // We are not supporting Windows containers at the moment - return DockerSocketPathWithSchema + if len(errs) > 0 { + return "", errors.Join(errs...) + } + + return "", ErrSocketNotFound } -// extractDockerHost Extracts the docker socket from the different alternatives, without caching the result. +// 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. -// If a Docker client cannot be created, the program will panic. +// 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 { @@ -140,6 +172,7 @@ func extractDockerSocket(ctx context.Context) string { // 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 { @@ -179,11 +212,33 @@ func extractDockerSocketFromClient(ctx context.Context, cli client.APIClient) st return DockerSocketPath } - dockerHost := extractDockerHost(ctx) + 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(ctx context.Context) (string, error) { if dockerHostPath := os.Getenv("DOCKER_HOST"); dockerHostPath != "" { 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 index 44782d31..b8e0f6e1 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_rootless.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/docker_rootless.go @@ -53,18 +53,24 @@ func rootlessDockerSocketPath(_ context.Context) (string, error) { rootlessSocketPathFromRunDir, } - outerErr := ErrRootlessDockerNotFound + var errs []error for _, socketPathFn := range socketPathFns { s, err := socketPathFn() if err != nil { - outerErr = fmt.Errorf("%w: %w", outerErr, err) + if !isHostNotSet(err) { + errs = append(errs, err) + } continue } return DockerSocketSchema + s, nil } - return "", outerErr + if len(errs) > 0 { + return "", errors.Join(errs...) + } + + return "", ErrRootlessDockerNotFound } func fileExists(f string) bool { diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go b/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go index 58b054ab..08149242 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/core/labels.go @@ -1,23 +1,73 @@ package core import ( + "errors" + "fmt" + "strings" + "github.com/testcontainers/testcontainers-go/internal" + "github.com/testcontainers/testcontainers-go/internal/config" ) const ( - LabelBase = "org.testcontainers" - LabelLang = LabelBase + ".lang" - LabelReaper = LabelBase + ".reaper" - LabelRyuk = LabelBase + ".ryuk" + // 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 = LabelBase + ".version" + + // 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 { - return map[string]string{ + labels := map[string]string{ LabelBase: "true", LabelLang: "go", - LabelSessionID: sessionID, 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/version.go b/vendor/github.com/testcontainers/testcontainers-go/internal/version.go index 3dc92975..0c688d5e 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/version.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/version.go @@ -1,4 +1,4 @@ package internal // Version is the next development version of the application -const Version = "0.33.0" +const Version = "0.34.0" diff --git a/vendor/github.com/testcontainers/testcontainers-go/lifecycle.go b/vendor/github.com/testcontainers/testcontainers-go/lifecycle.go index 40360a4c..57833daf 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/lifecycle.go +++ b/vendor/github.com/testcontainers/testcontainers-go/lifecycle.go @@ -33,7 +33,7 @@ type ContainerRequestHook func(ctx context.Context, req ContainerRequest) error // - Terminating // - Terminated // For that, it will receive a Container, modify it and return an error if needed. -type ContainerHook func(ctx context.Context, container Container) error +type ContainerHook func(ctx context.Context, ctr Container) error // ContainerLifecycleHooks is a struct that contains all the hooks that can be used // to modify the container lifecycle. All the container lifecycle hooks except the PreCreates hooks @@ -190,7 +190,6 @@ var defaultLogConsumersHook = func(cfg *LogConsumerConfig) ContainerLifecycleHoo } dockerContainer := c.(*DockerContainer) - return dockerContainer.stopLogProduction() }, }, @@ -411,10 +410,10 @@ func (c ContainerLifecycleHooks) Creating(ctx context.Context) func(req Containe // containerHookFn is a helper function that will create a function to be returned by all the different // container lifecycle hooks. The created function will iterate over all the hooks and call them one by one. func containerHookFn(ctx context.Context, containerHook []ContainerHook) func(container Container) error { - return func(container Container) error { + return func(ctr Container) error { errs := make([]error, len(containerHook)) for i, hook := range containerHook { - errs[i] = hook(ctx, container) + errs[i] = hook(ctx, ctr) } return errors.Join(errs...) diff --git a/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml b/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml index 7dc942de..2d80a5b4 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml +++ b/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml @@ -57,6 +57,7 @@ nav: - Introduction: features/wait/introduction.md - Exec: features/wait/exec.md - Exit: features/wait/exit.md + - File: features/wait/file.md - Health: features/wait/health.md - HostPort: features/wait/host_port.md - HTTP: features/wait/http.md @@ -73,8 +74,11 @@ nav: - modules/cockroachdb.md - modules/consul.md - modules/couchbase.md + - modules/databend.md - modules/dolt.md + - modules/dynamodb.md - modules/elasticsearch.md + - modules/etcd.md - modules/gcloud.md - modules/grafana-lgtm.md - modules/inbucket.md @@ -84,6 +88,7 @@ nav: - modules/kafka.md - modules/localstack.md - modules/mariadb.md + - modules/meilisearch.md - modules/milvus.md - modules/minio.md - modules/mockserver.md @@ -108,6 +113,7 @@ nav: - modules/vault.md - modules/vearch.md - modules/weaviate.md + - modules/yugabytedb.md - Examples: - examples/index.md - examples/nginx.md @@ -134,4 +140,4 @@ nav: - Getting help: getting_help.md edit_uri: edit/main/docs/ extra: - latest_version: v0.33.0 + latest_version: v0.34.0 diff --git a/vendor/github.com/testcontainers/testcontainers-go/options.go b/vendor/github.com/testcontainers/testcontainers-go/options.go index 6b5dcb12..2849b156 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/options.go +++ b/vendor/github.com/testcontainers/testcontainers-go/options.go @@ -3,6 +3,7 @@ package testcontainers import ( "context" "fmt" + "net/url" "time" "dario.cat/mergo" @@ -155,7 +156,12 @@ func (c CustomHubSubstitutor) Substitute(image string) (string, error) { } } - return fmt.Sprintf("%s/%s", c.hub, image), nil + result, err := url.JoinPath(c.hub, image) + if err != nil { + return "", err + } + + return result, nil } // prependHubRegistry represents a way to prepend a custom Hub registry to the image name, @@ -198,7 +204,12 @@ func (p prependHubRegistry) Substitute(image string) (string, error) { } } - return fmt.Sprintf("%s/%s", p.prefix, image), nil + result, err := url.JoinPath(p.prefix, image) + if err != nil { + return "", err + } + + return result, nil } // WithImageSubstitutors sets the image substitutors for a container diff --git a/vendor/github.com/testcontainers/testcontainers-go/parallel.go b/vendor/github.com/testcontainers/testcontainers-go/parallel.go index 34740eea..0349023b 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/parallel.go +++ b/vendor/github.com/testcontainers/testcontainers-go/parallel.go @@ -31,25 +31,28 @@ func (gpe ParallelContainersError) Error() string { return fmt.Sprintf("%v", gpe.Errors) } +// parallelContainersResult represents result. +type parallelContainersResult struct { + ParallelContainersRequestError + Container Container +} + func parallelContainersRunner( ctx context.Context, requests <-chan GenericContainerRequest, - errors chan<- ParallelContainersRequestError, - containers chan<- Container, + results chan<- parallelContainersResult, wg *sync.WaitGroup, ) { + defer wg.Done() for req := range requests { c, err := GenericContainer(ctx, req) + res := parallelContainersResult{Container: c} if err != nil { - errors <- ParallelContainersRequestError{ - Request: req, - Error: err, - } - continue + res.Request = req + res.Error = err } - containers <- c + results <- res } - wg.Done() } // ParallelContainers creates a generic containers with parameters and run it in parallel mode @@ -64,41 +67,26 @@ func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt } tasksChan := make(chan GenericContainerRequest, tasksChanSize) - errsChan := make(chan ParallelContainersRequestError) - resChan := make(chan Container) - waitRes := make(chan struct{}) - - containers := make([]Container, 0) - errors := make([]ParallelContainersRequestError, 0) + resultsChan := make(chan parallelContainersResult, tasksChanSize) + done := make(chan struct{}) - wg := sync.WaitGroup{} + var wg sync.WaitGroup wg.Add(tasksChanSize) // run workers for i := 0; i < tasksChanSize; i++ { - go parallelContainersRunner(ctx, tasksChan, errsChan, resChan, &wg) + go parallelContainersRunner(ctx, tasksChan, resultsChan, &wg) } + var errs []ParallelContainersRequestError + containers := make([]Container, 0, len(reqs)) go func() { - for { - select { - case c, ok := <-resChan: - if !ok { - resChan = nil - } else { - containers = append(containers, c) - } - case e, ok := <-errsChan: - if !ok { - errsChan = nil - } else { - errors = append(errors, e) - } - } - - if resChan == nil && errsChan == nil { - waitRes <- struct{}{} - break + defer close(done) + for res := range resultsChan { + if res.Error != nil { + errs = append(errs, res.ParallelContainersRequestError) + } else { + containers = append(containers, res.Container) } } }() @@ -107,14 +95,15 @@ func ParallelContainers(ctx context.Context, reqs ParallelContainerRequest, opt tasksChan <- req } close(tasksChan) + wg.Wait() - close(resChan) - close(errsChan) - <-waitRes + close(resultsChan) + + <-done - if len(errors) != 0 { - return containers, ParallelContainersError{Errors: errors} + if len(errs) != 0 { + return containers, ParallelContainersError{Errors: errs} } return containers, nil diff --git a/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go b/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go index 1d86a8cd..88f14f2d 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go +++ b/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go @@ -38,9 +38,7 @@ var sshPassword = uuid.NewString() // 1. Create a new SSHD container. // 2. Expose the host ports to the container after the container is ready. // 3. Close the SSH sessions before killing the container. -func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) (ContainerLifecycleHooks, error) { - var sshdConnectHook ContainerLifecycleHooks - +func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) (sshdConnectHook ContainerLifecycleHooks, err error) { //nolint:nonamedreturns // Required for error check. if len(ports) == 0 { return sshdConnectHook, fmt.Errorf("no ports to expose") } @@ -91,6 +89,12 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( // start the SSHD container with the provided options sshdContainer, err := newSshdContainer(ctx, opts...) + // Ensure the SSHD container is stopped and removed in case of error. + defer func() { + if err != nil { + err = errors.Join(err, TerminateContainer(sshdContainer)) + } + }() if err != nil { return sshdConnectHook, fmt.Errorf("new sshd container: %w", err) } @@ -129,6 +133,20 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( originalHCM(hostConfig) } + stopHooks := []ContainerHook{ + func(ctx context.Context, _ Container) error { + if ctx.Err() != nil { + // Context already canceled, need to create a new one to ensure + // the SSH session is closed. + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + } + + return TerminateContainer(sshdContainer, StopContext(ctx)) + }, + } + // after the container is ready, create the SSH tunnel // for each exposed port from the host. sshdConnectHook = ContainerLifecycleHooks{ @@ -137,12 +155,8 @@ func exposeHostPorts(ctx context.Context, req *ContainerRequest, ports ...int) ( return sshdContainer.exposeHostPort(ctx, req.HostAccessPorts...) }, }, - PreTerminates: []ContainerHook{ - func(ctx context.Context, _ Container) error { - // before killing the container, close the SSH sessions - return sshdContainer.Terminate(ctx) - }, - }, + PreStops: stopHooks, + PreTerminates: stopHooks, } return sshdConnectHook, nil @@ -168,17 +182,13 @@ func newSshdContainer(ctx context.Context, opts ...ContainerCustomizer) (*sshdCo } c, err := GenericContainer(ctx, req) - if err != nil { - return nil, err + var sshd *sshdContainer + if c != nil { + sshd = &sshdContainer{Container: c} } - // force a type assertion to return a concrete type, - // because GenericContainer returns a Container interface. - dc := c.(*DockerContainer) - - sshd := &sshdContainer{ - DockerContainer: dc, - portForwarders: []PortForwarder{}, + if err != nil { + return sshd, fmt.Errorf("generic container: %w", err) } sshClientConfig, err := configureSSHConfig(ctx, sshd) @@ -195,7 +205,7 @@ func newSshdContainer(ctx context.Context, opts ...ContainerCustomizer) (*sshdCo // sshdContainer represents the SSHD container type used for the port forwarding container. // It's an internal type that extends the DockerContainer type, to add the SSH tunneling capabilities. type sshdContainer struct { - *DockerContainer + Container port string sshConfig *ssh.ClientConfig portForwarders []PortForwarder @@ -203,17 +213,30 @@ type sshdContainer struct { // Terminate stops the container and closes the SSH session func (sshdC *sshdContainer) Terminate(ctx context.Context) error { + sshdC.closePorts(ctx) + + return sshdC.Container.Terminate(ctx) +} + +// Stop stops the container and closes the SSH session +func (sshdC *sshdContainer) Stop(ctx context.Context, timeout *time.Duration) error { + sshdC.closePorts(ctx) + + return sshdC.Container.Stop(ctx, timeout) +} + +// closePorts closes all port forwarders. +func (sshdC *sshdContainer) closePorts(ctx context.Context) { for _, pfw := range sshdC.portForwarders { pfw.Close(ctx) } - - return sshdC.DockerContainer.Terminate(ctx) + sshdC.portForwarders = nil // Ensure the port forwarders are not used after closing. } func configureSSHConfig(ctx context.Context, sshdC *sshdContainer) (*ssh.ClientConfig, error) { mappedPort, err := sshdC.MappedPort(ctx, sshPort) if err != nil { - return nil, err + return nil, fmt.Errorf("mapped port: %w", err) } sshdC.port = mappedPort.Port() diff --git a/vendor/github.com/testcontainers/testcontainers-go/provider.go b/vendor/github.com/testcontainers/testcontainers-go/provider.go index c5635030..b5e5ffa9 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/provider.go +++ b/vendor/github.com/testcontainers/testcontainers-go/provider.go @@ -147,7 +147,7 @@ func NewDockerProvider(provOpts ...DockerProviderOption) (*DockerProvider, error return &DockerProvider{ DockerProviderOptions: o, - host: core.ExtractDockerHost(ctx), + host: core.MustExtractDockerHost(ctx), client: c, config: config.Read(), }, nil diff --git a/vendor/github.com/testcontainers/testcontainers-go/reaper.go b/vendor/github.com/testcontainers/testcontainers-go/reaper.go index c17b4f32..8f2bde8a 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/reaper.go +++ b/vendor/github.com/testcontainers/testcontainers-go/reaper.go @@ -1,13 +1,16 @@ package testcontainers import ( - "bufio" + "bytes" "context" + "errors" "fmt" - "math/rand" + "io" "net" + "os" "strings" "sync" + "syscall" "time" "github.com/cenkalti/backoff/v4" @@ -34,9 +37,23 @@ const ( var ( // Deprecated: it has been replaced by an internal value ReaperDefaultImage = config.ReaperDefaultImage - reaperInstance *Reaper // We would like to create reaper only once - reaperMutex sync.Mutex - reaperOnce sync.Once + + // defaultReaperPort is the default port that the reaper listens on if not + // overridden by the RYUK_PORT environment variable. + defaultReaperPort = nat.Port("8080/tcp") + + // errReaperNotFound is returned when no reaper container is found. + errReaperNotFound = errors.New("reaper not found") + + // errReaperDisabled is returned if a reaper is requested but the + // config has it disabled. + errReaperDisabled = errors.New("reaper disabled") + + // spawner is the singleton instance of reaperSpawner. + spawner = &reaperSpawner{} + + // reaperAck is the expected response from the reaper container. + reaperAck = []byte("ACK\n") ) // ReaperProvider represents a provider for the reaper to run itself with @@ -47,10 +64,18 @@ type ReaperProvider interface { } // NewReaper creates a Reaper with a sessionID to identify containers and a provider to use -// Deprecated: it's not possible to create a reaper anymore. Compose module uses this method +// Deprecated: it's not possible to create a reaper any more. Compose module uses this method // to create a reaper for the compose stack. +// +// The caller must call Connect at least once on the returned Reaper and use the returned +// result otherwise the reaper will be kept open until the process exits. func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, reaperImageName string) (*Reaper, error) { - return reuseOrCreateReaper(ctx, sessionID, provider) + reaper, err := spawner.reaper(ctx, sessionID, provider) + if err != nil { + return nil, fmt.Errorf("reaper: %w", err) + } + + return reaper, nil } // reaperContainerNameFromSessionID returns the container name that uniquely @@ -61,31 +86,80 @@ func reaperContainerNameFromSessionID(sessionID string) string { return fmt.Sprintf("reaper_%s", sessionID) } -// lookUpReaperContainer returns a DockerContainer type with the reaper container in the case -// it's found in the running state, and including the labels for sessionID, reaper, and ryuk. -// It will perform a retry with exponential backoff to allow for the container to be started and -// avoid potential false negatives. -func lookUpReaperContainer(ctx context.Context, sessionID string) (*DockerContainer, error) { - dockerClient, err := NewDockerClientWithOpts(ctx) - if err != nil { - return nil, err +// reaperSpawner is a singleton that manages the reaper container. +type reaperSpawner struct { + instance *Reaper + mtx sync.Mutex +} + +// port returns the port that a new reaper should listens on. +func (r *reaperSpawner) port() nat.Port { + if port := os.Getenv("RYUK_PORT"); port != "" { + natPort, err := nat.NewPort("tcp", port) + if err != nil { + panic(fmt.Sprintf("invalid RYUK_PORT value %q: %s", port, err)) + } + return natPort } - defer dockerClient.Close() - // the backoff will take at most 5 seconds to find the reaper container - // doing each attempt every 100ms - exp := backoff.NewExponentialBackOff() + return defaultReaperPort +} - // we want random intervals between 100ms and 500ms for concurrent executions +// backoff returns a backoff policy for the reaper spawner. +// It will take at most 20 seconds, doing each attempt every 100ms - 250ms. +func (r *reaperSpawner) backoff() *backoff.ExponentialBackOff { + // We want random intervals between 100ms and 250ms for concurrent executions // to not be synchronized: it could be the case that multiple executions of this // function happen at the same time (specifically when called from a different test // process execution), and we want to avoid that they all try to find the reaper // container at the same time. - exp.InitialInterval = time.Duration(rand.Intn(5)*100) * time.Millisecond - exp.RandomizationFactor = rand.Float64() * 0.5 - exp.Multiplier = rand.Float64() * 2.0 - exp.MaxInterval = 5.0 * time.Second // max interval between attempts - exp.MaxElapsedTime = 1 * time.Minute // max time to keep trying + b := &backoff.ExponentialBackOff{ + InitialInterval: time.Millisecond * 100, + RandomizationFactor: backoff.DefaultRandomizationFactor, + Multiplier: backoff.DefaultMultiplier, + // Adjust MaxInterval to compensate for randomization factor which can be added to + // returned interval so we have a maximum of 250ms. + MaxInterval: time.Duration(float64(time.Millisecond*250) * backoff.DefaultRandomizationFactor), + MaxElapsedTime: time.Second * 20, + Stop: backoff.Stop, + Clock: backoff.SystemClock, + } + b.Reset() + + return b +} + +// cleanup terminates the reaper container if set. +func (r *reaperSpawner) cleanup() error { + r.mtx.Lock() + defer r.mtx.Unlock() + + return r.cleanupLocked() +} + +// cleanupLocked terminates the reaper container if set. +// It must be called with the lock held. +func (r *reaperSpawner) cleanupLocked() error { + if r.instance == nil { + return nil + } + + err := TerminateContainer(r.instance.container) + r.instance = nil + + return err +} + +// lookupContainer returns a DockerContainer type with the reaper container in the case +// it's found in the running state, and including the labels for sessionID, reaper, and ryuk. +// It will perform a retry with exponential backoff to allow for the container to be started and +// avoid potential false negatives. +func (r *reaperSpawner) lookupContainer(ctx context.Context, sessionID string) (*DockerContainer, error) { + dockerClient, err := NewDockerClientWithOpts(ctx) + if err != nil { + return nil, fmt.Errorf("new client: %w", err) + } + defer dockerClient.Close() opts := container.ListOptions{ All: true, @@ -97,159 +171,212 @@ func lookUpReaperContainer(ctx context.Context, sessionID string) (*DockerContai ), } - return backoff.RetryNotifyWithData( + return backoff.RetryWithData( func() (*DockerContainer, error) { resp, err := dockerClient.ContainerList(ctx, opts) if err != nil { - return nil, err + return nil, fmt.Errorf("container list: %w", err) } if len(resp) == 0 { - // reaper container not found in the running state: do not look for it again - return nil, nil + // No reaper container not found. + return nil, backoff.Permanent(errReaperNotFound) } if len(resp) > 1 { - return nil, fmt.Errorf("not possible to have multiple reaper containers found for session ID %s", sessionID) + return nil, fmt.Errorf("multiple reaper containers found for session ID %s", sessionID) } - r, err := containerFromDockerResponse(ctx, resp[0]) + container := resp[0] + r, err := containerFromDockerResponse(ctx, container) if err != nil { - return nil, err + return nil, fmt.Errorf("from docker: %w", err) } - if r.healthStatus == types.Healthy || r.healthStatus == types.NoHealthcheck { + switch { + case r.healthStatus == types.Healthy, + r.healthStatus == types.NoHealthcheck: return r, nil - } - - // if a health status is present on the container, and the container is healthy, error - if r.healthStatus != "" { - return nil, fmt.Errorf("container %s is not healthy, wanted status=%s, got status=%s", resp[0].ID[:8], types.Healthy, r.healthStatus) + case r.healthStatus != "": + return nil, fmt.Errorf("container not healthy: %s", r.healthStatus) } return r, nil }, - backoff.WithContext(exp, ctx), - func(err error, duration time.Duration) { - Logger.Printf("Error looking up reaper container, will retry: %v", err) - }, + backoff.WithContext(r.backoff(), ctx), ) } -// reuseOrCreateReaper returns an existing Reaper instance if it exists and is running. Otherwise, a new Reaper instance -// will be created with a sessionID to identify containers in the same test session/program. -func reuseOrCreateReaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { - reaperMutex.Lock() - defer reaperMutex.Unlock() - - // 1. if the reaper instance has been already created, return it - if reaperInstance != nil { - // Verify this instance is still running by checking state. - // Can't use Container.IsRunning because the bool is not updated when Reaper is terminated - state, err := reaperInstance.container.State(ctx) - if err != nil { - if !errdefs.IsNotFound(err) { - return nil, err +// isRunning returns an error if the container is not running. +func (r *reaperSpawner) isRunning(ctx context.Context, ctr Container) error { + state, err := ctr.State(ctx) + if err != nil { + return fmt.Errorf("container state: %w", err) + } + + if !state.Running { + // Use NotFound error to indicate the container is not running + // and should be recreated. + return errdefs.NotFound(fmt.Errorf("container state: %s", state.Status)) + } + + return nil +} + +// retryError returns a permanent error if the error is not considered retryable. +func (r *reaperSpawner) retryError(err error) error { + var timeout interface { + Timeout() bool + } + switch { + case isCleanupSafe(err), + createContainerFailDueToNameConflictRegex.MatchString(err.Error()), + errors.Is(err, syscall.ECONNREFUSED), + errors.Is(err, syscall.ECONNRESET), + errors.Is(err, syscall.ECONNABORTED), + errors.Is(err, syscall.ETIMEDOUT), + errors.Is(err, os.ErrDeadlineExceeded), + errors.As(err, &timeout) && timeout.Timeout(), + errors.Is(err, context.DeadlineExceeded), + errors.Is(err, context.Canceled): + // Retryable error. + return err + default: + return backoff.Permanent(err) + } +} + +// reaper returns an existing Reaper instance if it exists and is running, otherwise +// a new Reaper instance will be created with a sessionID to identify containers in +// the same test session/program. If connect is true, the reaper will be connected +// to the reaper container. +// Returns an error if config.RyukDisabled is true. +// +// Safe for concurrent calls. +func (r *reaperSpawner) reaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { + if config.Read().RyukDisabled { + return nil, errReaperDisabled + } + + r.mtx.Lock() + defer r.mtx.Unlock() + + return backoff.RetryWithData( + r.retryLocked(ctx, sessionID, provider), + backoff.WithContext(r.backoff(), ctx), + ) +} + +// retryLocked returns a function that can be used to create or reuse a reaper container. +// If connect is true, the reaper will be connected to the reaper container. +// It must be called with the lock held. +func (r *reaperSpawner) retryLocked(ctx context.Context, sessionID string, provider ReaperProvider) func() (*Reaper, error) { + return func() (reaper *Reaper, err error) { //nolint:nonamedreturns // Needed for deferred error check. + reaper, err = r.reuseOrCreate(ctx, sessionID, provider) + // Ensure that the reaper is terminated if an error occurred. + defer func() { + if err != nil { + if reaper != nil { + err = errors.Join(err, TerminateContainer(reaper.container)) + } + err = r.retryError(errors.Join(err, r.cleanupLocked())) } - } else if state.Running { - return reaperInstance, nil - } - // else: the reaper instance has been terminated, so we need to create a new one - reaperOnce = sync.Once{} - } - - // 2. because the reaper instance has not been created yet, look for it in the Docker daemon, which - // will happen if the reaper container has been created in the same test session but in a different - // test process execution (e.g. when running tests in parallel), not having initialized the reaper - // instance yet. - reaperContainer, err := lookUpReaperContainer(context.Background(), sessionID) - if err == nil && reaperContainer != nil { - // The reaper container exists as a Docker container: re-use it - Logger.Printf("🔥 Reaper obtained from Docker for this test session %s", reaperContainer.ID) - reaperInstance, err = reuseReaperContainer(ctx, sessionID, provider, reaperContainer) + }() if err != nil { return nil, err } - return reaperInstance, nil - } + if err = r.isRunning(ctx, reaper.container); err != nil { + return nil, err + } - // 3. the reaper container does not exist in the Docker daemon: create it, and do it using the - // synchronization primitive to avoid multiple executions of this function to create the reaper - var reaperErr error - reaperOnce.Do(func() { - r, err := newReaper(ctx, sessionID, provider) + // Check we can still connect. + termSignal, err := reaper.connect(ctx) if err != nil { - reaperErr = err - return + return nil, fmt.Errorf("connect: %w", err) } - reaperInstance, reaperErr = r, nil - }) - if reaperErr != nil { - reaperOnce = sync.Once{} - return nil, reaperErr - } + reaper.setOrSignal(termSignal) + + r.instance = reaper - return reaperInstance, nil + return reaper, nil + } } -// reuseReaperContainer constructs a Reaper from an already running reaper -// DockerContainer. -func reuseReaperContainer(ctx context.Context, sessionID string, provider ReaperProvider, reaperContainer *DockerContainer) (*Reaper, error) { - endpoint, err := reaperContainer.PortEndpoint(ctx, "8080", "") +// reuseOrCreate returns an existing Reaper instance if it exists, otherwise a new Reaper instance. +func (r *reaperSpawner) reuseOrCreate(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { + if r.instance != nil { + // We already have an associated reaper. + return r.instance, nil + } + + // Look for an existing reaper created in the same test session but in a + // different test process execution e.g. when running tests in parallel. + container, err := r.lookupContainer(context.Background(), sessionID) if err != nil { - return nil, err + if !errors.Is(err, errReaperNotFound) { + return nil, fmt.Errorf("look up container: %w", err) + } + + // The reaper container was not found, continue to create a new one. + reaper, err := r.newReaper(ctx, sessionID, provider) + if err != nil { + return nil, fmt.Errorf("new reaper: %w", err) + } + + return reaper, nil } - Logger.Printf("⏳ Waiting for Reaper port to be ready") + // A reaper container exists re-use it. + reaper, err := r.fromContainer(ctx, sessionID, provider, container) + if err != nil { + return nil, fmt.Errorf("from container %q: %w", container.ID[:8], err) + } + + return reaper, nil +} - var containerJson *types.ContainerJSON +// fromContainer constructs a Reaper from an already running reaper DockerContainer. +func (r *reaperSpawner) fromContainer(ctx context.Context, sessionID string, provider ReaperProvider, dockerContainer *DockerContainer) (*Reaper, error) { + Logger.Printf("⏳ Waiting for Reaper %q to be ready", dockerContainer.ID[:8]) - if containerJson, err = reaperContainer.Inspect(ctx); err != nil { - return nil, fmt.Errorf("failed to inspect reaper container %s: %w", reaperContainer.ID[:8], err) + // Reusing an existing container so we determine the port from the container's exposed ports. + if err := wait.ForExposedPort(). + WithPollInterval(100*time.Millisecond). + SkipInternalCheck(). + WaitUntilReady(ctx, dockerContainer); err != nil { + return nil, fmt.Errorf("wait for reaper %s: %w", dockerContainer.ID[:8], err) } - if containerJson != nil && containerJson.NetworkSettings != nil { - for port := range containerJson.NetworkSettings.Ports { - err := wait.ForListeningPort(port). - WithPollInterval(100*time.Millisecond). - WaitUntilReady(ctx, reaperContainer) - if err != nil { - return nil, fmt.Errorf("failed waiting for reaper container %s port %s/%s to be ready: %w", - reaperContainer.ID[:8], port.Proto(), port.Port(), err) - } - } + endpoint, err := dockerContainer.Endpoint(ctx, "") + if err != nil { + return nil, fmt.Errorf("port endpoint: %w", err) } + Logger.Printf("🔥 Reaper obtained from Docker for this test session %s", dockerContainer.ID[:8]) + return &Reaper{ Provider: provider, SessionID: sessionID, Endpoint: endpoint, - container: reaperContainer, + container: dockerContainer, }, nil } -// newReaper creates a Reaper with a sessionID to identify containers and a -// provider to use. Do not call this directly, use reuseOrCreateReaper instead. -func newReaper(ctx context.Context, sessionID string, provider ReaperProvider) (*Reaper, error) { - dockerHostMount := core.ExtractDockerSocket(ctx) - - reaper := &Reaper{ - Provider: provider, - SessionID: sessionID, - } - - listeningPort := nat.Port("8080/tcp") +// newReaper creates a connected Reaper with a sessionID to identify containers +// and a provider to use. +func (r *reaperSpawner) newReaper(ctx context.Context, sessionID string, provider ReaperProvider) (reaper *Reaper, err error) { //nolint:nonamedreturns // Needed for deferred error check. + dockerHostMount := core.MustExtractDockerSocket(ctx) + port := r.port() tcConfig := provider.Config().Config - req := ContainerRequest{ Image: config.ReaperDefaultImage, - ExposedPorts: []string{string(listeningPort)}, + ExposedPorts: []string{string(port)}, Labels: core.DefaultLabels(sessionID), Privileged: tcConfig.RyukPrivileged, - WaitingFor: wait.ForListeningPort(listeningPort), + WaitingFor: wait.ForListeningPort(port), Name: reaperContainerNameFromSessionID(sessionID), HostConfigModifier: func(hc *container.HostConfig) { hc.AutoRemove = true @@ -268,9 +395,10 @@ func newReaper(ctx context.Context, sessionID string, provider ReaperProvider) ( req.Env["RYUK_VERBOSE"] = "true" } - // include reaper-specific labels to the reaper container + // Setup reaper-specific labels for the reaper container. req.Labels[core.LabelReaper] = "true" req.Labels[core.LabelRyuk] = "true" + delete(req.Labels, core.LabelReap) // Attach reaper container to a requested network if it is specified if p, ok := provider.(*DockerProvider); ok { @@ -278,123 +406,158 @@ func newReaper(ctx context.Context, sessionID string, provider ReaperProvider) ( } c, err := provider.RunContainer(ctx, req) - if err != nil { - // We need to check whether the error is caused by a container with the same name - // already existing due to race conditions. We manually match the error message - // as we do not have any error types to check against. - if createContainerFailDueToNameConflictRegex.MatchString(err.Error()) { - // Manually retrieve the already running reaper container. However, we need to - // use retries here as there are two possible race conditions that might lead to - // errors: In most cases, there is a small delay between container creation and - // actually being visible in list-requests. This means that creation might fail - // due to name conflicts, but when we list containers with this name, we do not - // get any results. In another case, the container might have simply died in the - // meantime and therefore cannot be found. - const timeout = 5 * time.Second - const cooldown = 100 * time.Millisecond - start := time.Now() - var reaperContainer *DockerContainer - for time.Since(start) < timeout { - reaperContainer, err = lookUpReaperContainer(ctx, sessionID) - if err == nil && reaperContainer != nil { - break - } - select { - case <-ctx.Done(): - case <-time.After(cooldown): - } - } - if err != nil { - return nil, fmt.Errorf("look up reaper container due to name conflict failed: %w", err) - } - // If the reaper container was not found, it is most likely to have died in - // between as we can exclude any client errors because of the previous error - // check. Because the reaper should only die if it performed clean-ups, we can - // fail here as the reaper timeout needs to be increased, anyway. - if reaperContainer == nil { - return nil, fmt.Errorf("look up reaper container returned nil although creation failed due to name conflict") - } - Logger.Printf("🔥 Reaper obtained from Docker for this test session %s", reaperContainer.ID) - reaper, err := reuseReaperContainer(ctx, sessionID, provider, reaperContainer) - if err != nil { - return nil, err - } - return reaper, nil + defer func() { + if err != nil { + err = errors.Join(err, TerminateContainer(c)) } - return nil, err + }() + if err != nil { + return nil, fmt.Errorf("run container: %w", err) } - reaper.container = c - endpoint, err := c.PortEndpoint(ctx, "8080", "") + endpoint, err := c.PortEndpoint(ctx, port, "") if err != nil { - return nil, err + return nil, fmt.Errorf("port endpoint: %w", err) } - reaper.Endpoint = endpoint - return reaper, nil + return &Reaper{ + Provider: provider, + SessionID: sessionID, + Endpoint: endpoint, + container: c, + }, nil } // Reaper is used to start a sidecar container that cleans up resources type Reaper struct { - Provider ReaperProvider - SessionID string - Endpoint string - container Container + Provider ReaperProvider + SessionID string + Endpoint string + container Container + mtx sync.Mutex // Protects termSignal. + termSignal chan bool } -// Connect runs a goroutine which can be terminated by sending true into the returned channel +// Connect connects to the reaper container and sends the labels to it +// so that it can clean up the containers with the same labels. +// +// It returns a channel that can be closed to terminate the connection. +// Returns an error if config.RyukDisabled is true. func (r *Reaper) Connect() (chan bool, error) { - conn, err := net.DialTimeout("tcp", r.Endpoint, 10*time.Second) - if err != nil { - return nil, fmt.Errorf("%w: Connecting to Ryuk on %s failed", err, r.Endpoint) + if config.Read().RyukDisabled { + return nil, errReaperDisabled } - terminationSignal := make(chan bool) - go func(conn net.Conn) { - sock := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) - defer conn.Close() + if termSignal := r.useTermSignal(); termSignal != nil { + return termSignal, nil + } - labelFilters := []string{} - for l, v := range core.DefaultLabels(r.SessionID) { - labelFilters = append(labelFilters, fmt.Sprintf("label=%s=%s", l, v)) - } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() - retryLimit := 3 - for retryLimit > 0 { - retryLimit-- + return r.connect(ctx) +} - if _, err := sock.WriteString(strings.Join(labelFilters, "&")); err != nil { - continue - } +// close signals the connection to close if needed. +// Safe for concurrent calls. +func (r *Reaper) close() { + r.mtx.Lock() + defer r.mtx.Unlock() - if _, err := sock.WriteString("\n"); err != nil { - continue - } + if r.termSignal != nil { + r.termSignal <- true + r.termSignal = nil + } +} - if err := sock.Flush(); err != nil { - continue - } +// setOrSignal sets the reapers termSignal field if nil +// otherwise consumes by sending true to it. +// Safe for concurrent calls. +func (r *Reaper) setOrSignal(termSignal chan bool) { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.termSignal != nil { + // Already have an existing connection, close the new one. + termSignal <- true + return + } - resp, err := sock.ReadString('\n') - if err != nil { - continue - } + // First or new unused termSignal, assign for caller to reuse. + r.termSignal = termSignal +} - if resp == "ACK\n" { - break - } - } +// useTermSignal if termSignal is not nil returns it +// and sets it to nil, otherwise returns nil. +// +// Safe for concurrent calls. +func (r *Reaper) useTermSignal() chan bool { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.termSignal == nil { + return nil + } + + // Use existing connection. + term := r.termSignal + r.termSignal = nil + + return term +} +// connect connects to the reaper container and sends the labels to it +// so that it can clean up the containers with the same labels. +// +// It returns a channel that can be sent true to terminate the connection. +// Returns an error if config.RyukDisabled is true. +func (r *Reaper) connect(ctx context.Context) (chan bool, error) { + var d net.Dialer + conn, err := d.DialContext(ctx, "tcp", r.Endpoint) + if err != nil { + return nil, fmt.Errorf("dial reaper %s: %w", r.Endpoint, err) + } + + terminationSignal := make(chan bool) + go func() { + defer conn.Close() + if err := r.handshake(conn); err != nil { + Logger.Printf("Reaper handshake failed: %s", err) + } <-terminationSignal - }(conn) + }() return terminationSignal, nil } +// handshake sends the labels to the reaper container and reads the ACK. +func (r *Reaper) handshake(conn net.Conn) error { + labels := core.DefaultLabels(r.SessionID) + labelFilters := make([]string, 0, len(labels)) + for l, v := range labels { + labelFilters = append(labelFilters, fmt.Sprintf("label=%s=%s", l, v)) + } + + filters := []byte(strings.Join(labelFilters, "&") + "\n") + buf := make([]byte, 4) + if _, err := conn.Write(filters); err != nil { + return fmt.Errorf("writing filters: %w", err) + } + + n, err := io.ReadFull(conn, buf) + if err != nil { + return fmt.Errorf("read ack: %w", err) + } + + if !bytes.Equal(reaperAck, buf[:n]) { + // We have received the ACK so all done. + return fmt.Errorf("unexpected reaper response: %s", buf[:n]) + } + + return nil +} + // Labels returns the container labels to use so that this Reaper cleans them up // Deprecated: internally replaced by core.DefaultLabels(sessionID) func (r *Reaper) Labels() map[string]string { - return map[string]string{ - core.LabelLang: "go", - core.LabelSessionID: r.SessionID, - } + return GenericLabels() } diff --git a/vendor/github.com/testcontainers/testcontainers-go/sonar-project.properties b/vendor/github.com/testcontainers/testcontainers-go/sonar-project.properties index aaa203e9..67ef15fc 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/sonar-project.properties +++ b/vendor/github.com/testcontainers/testcontainers-go/sonar-project.properties @@ -7,7 +7,7 @@ sonar.projectKey=testcontainers_testcontainers-go sonar.projectName=testcontainers-go -sonar.projectVersion=v0.33.0 +sonar.projectVersion=v0.34.0 sonar.sources=. @@ -18,4 +18,4 @@ sonar.test.inclusions=**/*_test.go sonar.test.exclusions=**/vendor/** sonar.go.coverage.reportPaths=**/coverage.out -sonar.go.tests.reportPaths=TEST-unit.xml,examples/nginx/TEST-unit.xml,examples/toxiproxy/TEST-unit.xml,modulegen/TEST-unit.xml,modules/artemis/TEST-unit.xml,modules/azurite/TEST-unit.xml,modules/cassandra/TEST-unit.xml,modules/chroma/TEST-unit.xml,modules/clickhouse/TEST-unit.xml,modules/cockroachdb/TEST-unit.xml,modules/compose/TEST-unit.xml,modules/consul/TEST-unit.xml,modules/couchbase/TEST-unit.xml,modules/dolt/TEST-unit.xml,modules/elasticsearch/TEST-unit.xml,modules/gcloud/TEST-unit.xml,modules/grafana-lgtm/TEST-unit.xml,modules/inbucket/TEST-unit.xml,modules/influxdb/TEST-unit.xml,modules/k3s/TEST-unit.xml,modules/k6/TEST-unit.xml,modules/kafka/TEST-unit.xml,modules/localstack/TEST-unit.xml,modules/mariadb/TEST-unit.xml,modules/milvus/TEST-unit.xml,modules/minio/TEST-unit.xml,modules/mockserver/TEST-unit.xml,modules/mongodb/TEST-unit.xml,modules/mssql/TEST-unit.xml,modules/mysql/TEST-unit.xml,modules/nats/TEST-unit.xml,modules/neo4j/TEST-unit.xml,modules/ollama/TEST-unit.xml,modules/openfga/TEST-unit.xml,modules/openldap/TEST-unit.xml,modules/opensearch/TEST-unit.xml,modules/postgres/TEST-unit.xml,modules/pulsar/TEST-unit.xml,modules/qdrant/TEST-unit.xml,modules/rabbitmq/TEST-unit.xml,modules/redis/TEST-unit.xml,modules/redpanda/TEST-unit.xml,modules/registry/TEST-unit.xml,modules/surrealdb/TEST-unit.xml,modules/valkey/TEST-unit.xml,modules/vault/TEST-unit.xml,modules/vearch/TEST-unit.xml,modules/weaviate/TEST-unit.xml +sonar.go.tests.reportPaths=TEST-unit.xml,examples/nginx/TEST-unit.xml,examples/toxiproxy/TEST-unit.xml,modulegen/TEST-unit.xml,modules/artemis/TEST-unit.xml,modules/azurite/TEST-unit.xml,modules/cassandra/TEST-unit.xml,modules/chroma/TEST-unit.xml,modules/clickhouse/TEST-unit.xml,modules/cockroachdb/TEST-unit.xml,modules/compose/TEST-unit.xml,modules/consul/TEST-unit.xml,modules/couchbase/TEST-unit.xml,modules/databend/TEST-unit.xml,modules/dolt/TEST-unit.xml,modules/dynamodb/TEST-unit.xml,modules/elasticsearch/TEST-unit.xml,modules/etcd/TEST-unit.xml,modules/gcloud/TEST-unit.xml,modules/grafana-lgtm/TEST-unit.xml,modules/inbucket/TEST-unit.xml,modules/influxdb/TEST-unit.xml,modules/k3s/TEST-unit.xml,modules/k6/TEST-unit.xml,modules/kafka/TEST-unit.xml,modules/localstack/TEST-unit.xml,modules/mariadb/TEST-unit.xml,modules/meilisearch/TEST-unit.xml,modules/milvus/TEST-unit.xml,modules/minio/TEST-unit.xml,modules/mockserver/TEST-unit.xml,modules/mongodb/TEST-unit.xml,modules/mssql/TEST-unit.xml,modules/mysql/TEST-unit.xml,modules/nats/TEST-unit.xml,modules/neo4j/TEST-unit.xml,modules/ollama/TEST-unit.xml,modules/openfga/TEST-unit.xml,modules/openldap/TEST-unit.xml,modules/opensearch/TEST-unit.xml,modules/postgres/TEST-unit.xml,modules/pulsar/TEST-unit.xml,modules/qdrant/TEST-unit.xml,modules/rabbitmq/TEST-unit.xml,modules/redis/TEST-unit.xml,modules/redpanda/TEST-unit.xml,modules/registry/TEST-unit.xml,modules/surrealdb/TEST-unit.xml,modules/valkey/TEST-unit.xml,modules/vault/TEST-unit.xml,modules/vearch/TEST-unit.xml,modules/weaviate/TEST-unit.xml,modules/yugabytedb/TEST-unit.xml diff --git a/vendor/github.com/testcontainers/testcontainers-go/testcontainers.go b/vendor/github.com/testcontainers/testcontainers-go/testcontainers.go index 5b52e09a..7ae4a40c 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/testcontainers.go +++ b/vendor/github.com/testcontainers/testcontainers-go/testcontainers.go @@ -6,7 +6,12 @@ import ( "github.com/testcontainers/testcontainers-go/internal/core" ) -// ExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema. +// Deprecated: use MustExtractDockerHost instead. +func ExtractDockerSocket() string { + return MustExtractDockerSocket(context.Background()) +} + +// MustExtractDockerSocket Extracts the docker socket from the different alternatives, removing the socket schema. // 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: @@ -14,13 +19,13 @@ import ( // 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. +// 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) // -// In any case, if the docker socket schema is "tcp://", the default docker socket path will be returned. -func ExtractDockerSocket() string { - return core.ExtractDockerSocket(context.Background()) +// It panics if a Docker client cannot be created, or the Docker host cannot be discovered. +func MustExtractDockerSocket(ctx context.Context) string { + return core.MustExtractDockerSocket(ctx) } // SessionID returns a unique session ID for the current test session. Because each Go package diff --git a/vendor/github.com/testcontainers/testcontainers-go/testdata/.docker/config.json b/vendor/github.com/testcontainers/testcontainers-go/testdata/.docker/config.json deleted file mode 100644 index af4b84ef..00000000 --- a/vendor/github.com/testcontainers/testcontainers-go/testdata/.docker/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "auths": { - "https://index.docker.io/v1/": {}, - "https://example.com": {}, - "https://my.private.registry": {} - }, - "credsStore": "desktop" -} \ No newline at end of file diff --git a/vendor/github.com/testcontainers/testcontainers-go/testing.go b/vendor/github.com/testcontainers/testcontainers-go/testing.go index eab23cb8..0601d9fa 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/testing.go +++ b/vendor/github.com/testcontainers/testcontainers-go/testing.go @@ -3,14 +3,23 @@ package testcontainers import ( "context" "fmt" + "regexp" "testing" + + "github.com/docker/docker/errdefs" + "github.com/stretchr/testify/require" ) +// errAlreadyInProgress is a regular expression that matches the error for a container +// removal that is already in progress. +var errAlreadyInProgress = regexp.MustCompile(`removal of container .* is already in progress`) + // SkipIfProviderIsNotHealthy is a utility function capable of skipping tests // if the provider is not healthy, or running at all. // This is a function designed to be used in your test, when Docker is not mandatory for CI/CD. // In this way tests that depend on Testcontainers won't run if the provider is provisioned correctly. func SkipIfProviderIsNotHealthy(t *testing.T) { + t.Helper() ctx := context.Background() provider, err := ProviderDocker.GetProvider() if err != nil { @@ -25,6 +34,7 @@ func SkipIfProviderIsNotHealthy(t *testing.T) { // SkipIfDockerDesktop is a utility function capable of skipping tests // if tests are run using Docker Desktop. func SkipIfDockerDesktop(t *testing.T, ctx context.Context) { + t.Helper() cli, err := NewDockerClientWithOpts(ctx) if err != nil { t.Fatalf("failed to create docker client: %s", err) @@ -51,3 +61,93 @@ func (lc *StdoutLogConsumer) Accept(l Log) { } // } + +// CleanupContainer is a helper function that schedules the container +// to be stopped / terminated when the test ends. +// +// This should be called as a defer directly after (before any error check) +// of [GenericContainer](...) or a modules Run(...) in a test to ensure the +// container is stopped when the function ends. +// +// before any error check. If container is nil, its a no-op. +func CleanupContainer(tb testing.TB, ctr Container, options ...TerminateOption) { + tb.Helper() + + tb.Cleanup(func() { + noErrorOrIgnored(tb, TerminateContainer(ctr, options...)) + }) +} + +// CleanupNetwork is a helper function that schedules the network to be +// removed when the test ends. +// This should be the first call after NewNetwork(...) in a test before +// any error check. If network is nil, its a no-op. +func CleanupNetwork(tb testing.TB, network Network) { + tb.Helper() + + tb.Cleanup(func() { + noErrorOrIgnored(tb, network.Remove(context.Background())) + }) +} + +// noErrorOrIgnored is a helper function that checks if the error is nil or an error +// we can ignore. +func noErrorOrIgnored(tb testing.TB, err error) { + tb.Helper() + + if isCleanupSafe(err) { + return + } + + require.NoError(tb, err) +} + +// causer is an interface that allows to get the cause of an error. +type causer interface { + Cause() error +} + +// wrapErr is an interface that allows to unwrap an error. +type wrapErr interface { + Unwrap() error +} + +// unwrapErrs is an interface that allows to unwrap multiple errors. +type unwrapErrs interface { + Unwrap() []error +} + +// isCleanupSafe reports whether all errors in err's tree are one of the +// following, so can safely be ignored: +// - nil +// - not found +// - already in progress +func isCleanupSafe(err error) bool { + if err == nil { + return true + } + + switch x := err.(type) { //nolint:errorlint // We need to check for interfaces. + case errdefs.ErrNotFound: + return true + case errdefs.ErrConflict: + // Terminating a container that is already terminating. + if errAlreadyInProgress.MatchString(err.Error()) { + return true + } + return false + case causer: + return isCleanupSafe(x.Cause()) + case wrapErr: + return isCleanupSafe(x.Unwrap()) + case unwrapErrs: + for _, e := range x.Unwrap() { + if !isCleanupSafe(e) { + return false + } + } + return true + default: + return false + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/file.go b/vendor/github.com/testcontainers/testcontainers-go/wait/file.go new file mode 100644 index 00000000..d9cab7a6 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/file.go @@ -0,0 +1,112 @@ +package wait + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/docker/docker/errdefs" +) + +var ( + _ Strategy = (*FileStrategy)(nil) + _ StrategyTimeout = (*FileStrategy)(nil) +) + +// FileStrategy waits for a file to exist in the container. +type FileStrategy struct { + timeout *time.Duration + file string + pollInterval time.Duration + matcher func(io.Reader) error +} + +// NewFileStrategy constructs an FileStrategy strategy. +func NewFileStrategy(file string) *FileStrategy { + return &FileStrategy{ + file: file, + pollInterval: defaultPollInterval(), + } +} + +// WithStartupTimeout can be used to change the default startup timeout +func (ws *FileStrategy) WithStartupTimeout(startupTimeout time.Duration) *FileStrategy { + ws.timeout = &startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *FileStrategy) WithPollInterval(pollInterval time.Duration) *FileStrategy { + ws.pollInterval = pollInterval + return ws +} + +// WithMatcher can be used to consume the file content. +// The matcher can return an errdefs.ErrNotFound to indicate that the file is not ready. +// Any other error will be considered a failure. +// Default: nil, will only wait for the file to exist. +func (ws *FileStrategy) WithMatcher(matcher func(io.Reader) error) *FileStrategy { + ws.matcher = matcher + return ws +} + +// ForFile is a convenience method to assign FileStrategy +func ForFile(file string) *FileStrategy { + return NewFileStrategy(file) +} + +// Timeout returns the timeout for the strategy +func (ws *FileStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady waits until the file exists in the container and copies it to the target. +func (ws *FileStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + timer := time.NewTicker(ws.pollInterval) + defer timer.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + if err := ws.matchFile(ctx, target); err != nil { + if errdefs.IsNotFound(err) { + // Not found, continue polling. + continue + } + + return fmt.Errorf("copy from container: %w", err) + } + return nil + } + } +} + +// matchFile tries to copy the file from the container and match it. +func (ws *FileStrategy) matchFile(ctx context.Context, target StrategyTarget) error { + rc, err := target.CopyFileFromContainer(ctx, ws.file) + if err != nil { + return fmt.Errorf("copy from container: %w", err) + } + defer rc.Close() + + if ws.matcher == nil { + // No matcher, just check if the file exists. + return nil + } + + if err = ws.matcher(rc); err != nil { + return fmt.Errorf("matcher: %w", err) + } + + return nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go b/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go index b349cc03..9360517a 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go @@ -13,13 +13,21 @@ import ( "github.com/docker/go-connections/nat" ) +const ( + exitEaccess = 126 // container cmd can't be invoked (permission denied) + exitCmdNotFound = 127 // container cmd not found/does not exist or invalid bind-mount +) + // Implement interface var ( _ Strategy = (*HostPortStrategy)(nil) _ StrategyTimeout = (*HostPortStrategy)(nil) ) -var errShellNotExecutable = errors.New("/bin/sh command not executable") +var ( + errShellNotExecutable = errors.New("/bin/sh command not executable") + errShellNotFound = errors.New("/bin/sh command not found") +) type HostPortStrategy struct { // Port is a string containing port number and protocol in the format "80/tcp" @@ -130,31 +138,37 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT select { case <-ctx.Done(): - return fmt.Errorf("%w: %w", ctx.Err(), err) + return fmt.Errorf("mapped port: retries: %d, port: %q, last err: %w, ctx err: %w", i, port, err, ctx.Err()) case <-time.After(waitInterval): if err := checkTarget(ctx, target); err != nil { - return err + return fmt.Errorf("check target: retries: %d, port: %q, last err: %w", i, port, err) } port, err = target.MappedPort(ctx, internalPort) if err != nil { - log.Printf("(%d) [%s] %s\n", i, port, err) + log.Printf("mapped port: retries: %d, port: %q, err: %s\n", i, port, err) } } } if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil { - return err + return fmt.Errorf("external check: %w", err) } if hp.skipInternalCheck { return nil } - err = internalCheck(ctx, internalPort, target) - if err != nil && errors.Is(errShellNotExecutable, err) { - log.Println("Shell not executable in container, only external port check will be performed") - } else { - return err + if err = internalCheck(ctx, internalPort, target); err != nil { + switch { + case errors.Is(err, errShellNotExecutable): + log.Println("Shell not executable in container, only external port validated") + return nil + case errors.Is(err, errShellNotFound): + log.Println("Shell not found in container") + return nil + default: + return fmt.Errorf("internal check: %w", err) + } } return nil @@ -167,9 +181,9 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target dialer := net.Dialer{} address := net.JoinHostPort(ipAddress, portString) - for { + for i := 0; ; i++ { if err := checkTarget(ctx, target); err != nil { - return err + return fmt.Errorf("check target: retries: %d address: %s: %w", i, address, err) } conn, err := dialer.DialContext(ctx, proto, address) if err != nil { @@ -183,7 +197,7 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target } } } - return err + return fmt.Errorf("dial: %w", err) } conn.Close() @@ -205,13 +219,18 @@ func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTa return fmt.Errorf("%w, host port waiting failed", err) } - if exitCode == 0 { - break - } else if exitCode == 126 { + // Docker has a issue which override exit code 127 to 126 due to: + // https://github.com/moby/moby/issues/45795 + // Handle both to ensure compatibility with Docker and Podman for now. + switch exitCode { + case 0: + return nil + case exitEaccess: return errShellNotExecutable + case exitCmdNotFound: + return errShellNotFound } } - return nil } func buildInternalCheckCommand(internalPort int) string { diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go b/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go index 7b8e918a..4206eefc 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go @@ -75,3 +75,7 @@ func (st NopStrategyTarget) Exec(_ context.Context, _ []string, _ ...exec.Proces func (st NopStrategyTarget) State(_ context.Context) (*types.ContainerState, error) { return &st.ContainerState, nil } + +func (st NopStrategyTarget) CopyFileFromContainer(context.Context, string) (io.ReadCloser, error) { + return st.ReaderCloser, nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go b/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go index ccfd4735..7211d49b 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go @@ -31,6 +31,7 @@ type StrategyTarget interface { Logs(context.Context) (io.ReadCloser, error) Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error) State(context.Context) (*types.ContainerState, error) + CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) } func checkTarget(ctx context.Context, target StrategyTarget) error { diff --git a/vendor/modules.txt b/vendor/modules.txt index 6d8566ec..6e8b59cb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -26,7 +26,7 @@ github.com/containerd/log # github.com/containerd/platforms v0.2.1 ## explicit; go 1.20 github.com/containerd/platforms -# github.com/cpuguy83/dockercfg v0.3.1 +# github.com/cpuguy83/dockercfg v0.3.2 ## explicit; go 1.13 github.com/cpuguy83/dockercfg # github.com/davecgh/go-spew v1.1.1 @@ -193,8 +193,8 @@ github.com/spf13/pflag github.com/stretchr/testify/assert github.com/stretchr/testify/require github.com/stretchr/testify/suite -# github.com/testcontainers/testcontainers-go v0.33.0 -## explicit; go 1.21 +# github.com/testcontainers/testcontainers-go v0.34.0 +## explicit; go 1.22 github.com/testcontainers/testcontainers-go github.com/testcontainers/testcontainers-go/exec github.com/testcontainers/testcontainers-go/internal