├── logger.go ├── doc.go ├── mockdockerclient ├── doc.go ├── error.go ├── io.go ├── image_api_client.go └── container_api_client.go ├── errors.go ├── .gitignore ├── rand_internal_test.go ├── .golangci.yml ├── rand.go ├── .travis.yml ├── .github └── workflows │ └── go.yml ├── .circleci └── config.yml ├── go.mod ├── options_internal_test.go ├── options.go ├── container_info_test.go ├── example_test.go ├── dktest_test.go ├── container_info.go ├── container_info_internal_test.go ├── README.md ├── dktest.go ├── dktest_internal_test.go ├── LICENSE └── go.sum /logger.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | // Logger is the interface used to log messages. 4 | type Logger interface { 5 | Log(...interface{}) 6 | } 7 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package dktest provides an easy way to write integration tests using Docker 2 | // 3 | // dktest is short for dockertest 4 | package dktest 5 | -------------------------------------------------------------------------------- /mockdockerclient/doc.go: -------------------------------------------------------------------------------- 1 | // Package mockdockerclient provides mocks for the Docker client 2 | // [github.com/docker/docker/client] 3 | package mockdockerclient 4 | -------------------------------------------------------------------------------- /mockdockerclient/error.go: -------------------------------------------------------------------------------- 1 | package mockdockerclient 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Err is the canonical error returned by mocks 8 | var Err = errors.New("mockdockerclient error") 9 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | errNoNetworkSettings = errors.New("no network settings") 9 | errNoPort = errors.New("no port") 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | -------------------------------------------------------------------------------- /rand_internal_test.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestRandString(t *testing.T) { 9 | sizes := []uint{0, 1, 2, 10, 100} 10 | 11 | for _, s := range sizes { 12 | t.Run(fmt.Sprintf("size %d", s), func(t *testing.T) { 13 | str := randString(s) 14 | if uint(len(str)) != s { 15 | t.Error("Got wrong randString size:", len(str), "!=", s) 16 | } 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - revive 4 | - unconvert 5 | - dupl 6 | - goconst 7 | - gofmt 8 | - misspell 9 | - unparam 10 | - nakedret 11 | - prealloc 12 | - gosec 13 | linters-settings: 14 | misspell: 15 | locale: US 16 | issues: 17 | max-same-issues: 0 18 | max-issues-per-linter: 0 19 | exclude-use-default: false 20 | exclude: 21 | # gosec: Duplicated errcheck checks 22 | - G104 23 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "math/rand/v2" 5 | ) 6 | 7 | const ( 8 | chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 9 | containerNamePrefix = "dktest_" 10 | ) 11 | 12 | func randString(n uint) string { 13 | if n == 0 { 14 | return "" 15 | } 16 | b := make([]byte, n) 17 | for i := range b { 18 | b[i] = chars[rand.IntN(len(chars))] // nolint:gosec 19 | } 20 | return string(b) 21 | } 22 | 23 | func genContainerName() string { return containerNamePrefix + randString(10) } 24 | -------------------------------------------------------------------------------- /mockdockerclient/io.go: -------------------------------------------------------------------------------- 1 | package mockdockerclient 2 | 3 | // MockReader is a mock implementation of the io.Reader interface 4 | type MockReader struct { 5 | N int 6 | Err error 7 | } 8 | 9 | // Read is a mock implementation of io.Reader.Read() 10 | func (r MockReader) Read(_ []byte) (n int, err error) { 11 | return r.N, r.Err 12 | } 13 | 14 | // MockCloser is a mock implementation of the io.Closer interface 15 | type MockCloser struct { 16 | Err error 17 | } 18 | 19 | // Close is a mock implementation of io.Closer.Close() 20 | func (c MockCloser) Close() error { return c.Err } 21 | 22 | // MockReadCloser is a mock implementation of the io.ReadCloser interface 23 | type MockReadCloser struct { 24 | MockReader 25 | MockCloser 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.14.x" 5 | - "1.15.x" 6 | - master 7 | 8 | matrix: 9 | allow_failures: 10 | - go: master 11 | 12 | services: 13 | - docker 14 | 15 | env: 16 | global: 17 | - GO111MODULE=on 18 | - GOLANGCI_LINT_VERSION=v1.43.0 19 | 20 | cache: 21 | directories: 22 | - $GOPATH/pkg 23 | - $GOCACHE 24 | 25 | before_install: 26 | # Update docker to latest version: https://docs.travis-ci.com/user/docker/#installing-a-newer-docker-version 27 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 28 | - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 29 | - sudo apt-get update 30 | - sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce 31 | # Install golangci-lint 32 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin $GOLANGCI_LINT_VERSION 33 | 34 | before_script: 35 | - golangci-lint run 36 | 37 | script: 38 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 39 | 40 | after_success: 41 | - bash <(curl -s https://codecov.io/bash) -X fix 42 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.25' 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v6 20 | with: 21 | version: v1.64.8 22 | 23 | test: 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | matrix: 28 | go: [ '1.24', '1.25' ] 29 | 30 | name: Go ${{ matrix.go }} 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v5 37 | with: 38 | go-version: ${{ matrix.go }} 39 | 40 | - name: Test 41 | run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 42 | 43 | - name: Send coverage to Coveralls 44 | uses: shogo82148/actions-goveralls@v1 45 | with: 46 | path-to-profile: coverage.txt 47 | flag-name: Go-${{ matrix.go }} 48 | parallel: true 49 | 50 | check-coverage: 51 | name: Check coverage 52 | needs: [test] 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: shogo82148/actions-goveralls@v1 56 | with: 57 | parallel-finished: true 58 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2.1 5 | 6 | jobs: 7 | "golang-1_18": &template 8 | machine: 9 | # https://circleci.com/docs/2.0/configuration-reference/#available-machine-images 10 | image: ubuntu-2004:202107-01 11 | 12 | # https://circleci.com/docs/2.0/configuration-reference/#resource_class 13 | resource_class: medium 14 | 15 | # Leave working directory unspecified and use defaults: 16 | # https://circleci.com/blog/go-v1.11-modules-and-circleci/ 17 | # working_directory: /go/src/github.com/golang-migrate/migrate 18 | 19 | environment: 20 | GO111MODULE: "on" 21 | GO_VERSION: "1.18.x" 22 | 23 | steps: 24 | - run: curl -sL -o ~/bin/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme 25 | - run: curl -sfL -o ~/bin/install-golangci-lint.sh https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh 26 | - run: chmod +x ~/bin/gimme ~/bin/install-golangci-lint.sh 27 | - run: eval "$(gimme $GO_VERSION)" 28 | - run: install-golangci-lint.sh -b ~/bin v1.41.1 29 | - checkout 30 | - restore_cache: 31 | keys: 32 | - go-mod-v1-{{ arch }}-{{ checksum "go.sum" }} 33 | - run: golangci-lint run 34 | - run: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 35 | - save_cache: 36 | key: go-mod-v1-{{ arch }}-{{ checksum "go.sum" }} 37 | paths: 38 | - "/go/pkg/mod" 39 | - run: go get github.com/mattn/goveralls 40 | - run: goveralls -service=circle-ci -coverprofile coverage.txt 41 | 42 | "golang-1_19": 43 | <<: *template 44 | environment: 45 | GO_VERSION: "1.19.x" 46 | 47 | 48 | workflows: 49 | version: 2 50 | build: 51 | jobs: 52 | - "golang-1_18" 53 | - "golang-1_19" 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dhui/dktest 2 | 3 | require ( 4 | github.com/docker/docker v28.3.3+incompatible 5 | github.com/docker/go-connections v0.4.0 6 | github.com/lib/pq v1.8.0 7 | github.com/opencontainers/image-spec v1.0.2 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 13 | github.com/Microsoft/go-winio v0.5.1 // indirect 14 | github.com/containerd/errdefs v1.0.0 // indirect 15 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 16 | github.com/containerd/log v0.1.0 // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/distribution/reference v0.6.0 // indirect 19 | github.com/docker/go-units v0.4.0 // indirect 20 | github.com/felixge/httpsnoop v1.0.4 // indirect 21 | github.com/go-logr/logr v1.4.3 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/moby/docker-image-spec v1.3.1 // indirect 25 | github.com/moby/sys/atomicwriter v0.1.0 // indirect 26 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 27 | github.com/morikuni/aec v1.0.0 // indirect 28 | github.com/opencontainers/go-digest v1.0.0 // indirect 29 | github.com/pkg/errors v0.9.1 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 32 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect 33 | go.opentelemetry.io/otel v1.37.0 // indirect 34 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect 35 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 36 | go.opentelemetry.io/otel/sdk v1.27.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 38 | golang.org/x/net v0.38.0 // indirect 39 | golang.org/x/sys v0.31.0 // indirect 40 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | gotest.tools/v3 v3.1.0 // indirect 43 | ) 44 | 45 | go 1.23.0 46 | -------------------------------------------------------------------------------- /options_internal_test.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | import ( 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestOptionsInit(t *testing.T) { 13 | timeout := 9 * time.Second 14 | 15 | testCases := []struct { 16 | name string 17 | opts Options 18 | expected Options 19 | }{ 20 | {name: "default timeouts used", opts: Options{}, 21 | expected: Options{ 22 | PullTimeout: DefaultPullTimeout, 23 | Timeout: DefaultTimeout, 24 | ReadyTimeout: DefaultReadyTimeout, 25 | CleanupTimeout: DefaultCleanupTimeout, 26 | }, 27 | }, 28 | {name: "default timeouts not used", 29 | opts: Options{ 30 | PullTimeout: timeout, 31 | Timeout: timeout, 32 | ReadyTimeout: timeout, 33 | CleanupTimeout: timeout, 34 | }, 35 | expected: Options{ 36 | PullTimeout: timeout, 37 | Timeout: timeout, 38 | ReadyTimeout: timeout, 39 | CleanupTimeout: timeout, 40 | }, 41 | }, 42 | } 43 | 44 | for _, tc := range testCases { 45 | t.Run(tc.name, func(t *testing.T) { 46 | tc.opts.init() 47 | assert.Equal(t, tc.expected, tc.opts, "Expected post-init Options to match expected") 48 | }) 49 | } 50 | } 51 | 52 | func TestOptionsEnv(t *testing.T) { 53 | testCases := []struct { 54 | name string 55 | env map[string]string 56 | expectedEnv []string 57 | }{ 58 | {name: "nil", env: nil, expectedEnv: nil}, 59 | {name: "empty", env: nil, expectedEnv: nil}, 60 | {name: "1 var", env: map[string]string{"foo": "bar"}, expectedEnv: []string{"foo=bar"}}, 61 | {name: "1 var - empty value", env: map[string]string{"foo": ""}, expectedEnv: []string{"foo="}}, 62 | {name: "1 var - empty key", env: map[string]string{"": "bar"}, expectedEnv: []string{"=bar"}}, 63 | {name: "3 vars", env: map[string]string{"foo": "bar", "hello": "world", "dead": "beef"}, 64 | expectedEnv: []string{"foo=bar", "hello=world", "dead=beef"}}, 65 | } 66 | 67 | for _, tc := range testCases { 68 | t.Run(tc.name, func(t *testing.T) { 69 | opts := Options{Env: tc.env} 70 | assert.ElementsMatch(t, tc.expectedEnv, opts.env(), "Options environment to match expected") 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/docker/docker/api/types/mount" 8 | "github.com/docker/go-connections/nat" 9 | ) 10 | 11 | // Options contains the configurable options for running tests in the docker image 12 | type Options struct { 13 | // PullTimeout is the timeout used when pulling images 14 | PullTimeout time.Duration 15 | // PullRegistryAuth is the base64 encoded credentials for the registry 16 | PullRegistryAuth string 17 | // Timeout is the timeout used when starting a container and checking if it's ready 18 | Timeout time.Duration 19 | // ReadyTimeout is the timeout used for each container ready check. 20 | // e.g. each invocation of the ReadyFunc 21 | ReadyTimeout time.Duration 22 | // CleanupTimeout is the timeout used when stopping and removing a container 23 | CleanupTimeout time.Duration 24 | // CleanupImage specifies whether or not the image should be removed after the test run. 25 | // If the image is used by multiple tests, you'll want to cleanup the image yourself. 26 | CleanupImage bool 27 | ReadyFunc func(context.Context, ContainerInfo) bool 28 | Env map[string]string 29 | Entrypoint []string 30 | Cmd []string 31 | // If you prefer to specify your port bindings as a string, use nat.ParsePortSpecs() 32 | PortBindings nat.PortMap 33 | PortRequired bool 34 | LogStdout bool 35 | LogStderr bool 36 | ShmSize int64 37 | Volumes []string 38 | Mounts []mount.Mount 39 | Hostname string 40 | // Platform specifies the platform of the docker image that is pulled. 41 | Platform string 42 | ExposedPorts nat.PortSet 43 | } 44 | 45 | func (o *Options) init() { 46 | if o.PullTimeout <= 0 { 47 | o.PullTimeout = DefaultPullTimeout 48 | } 49 | if o.Timeout <= 0 { 50 | o.Timeout = DefaultTimeout 51 | } 52 | if o.ReadyTimeout <= 0 { 53 | o.ReadyTimeout = DefaultReadyTimeout 54 | } 55 | if o.CleanupTimeout <= 0 { 56 | o.CleanupTimeout = DefaultCleanupTimeout 57 | } 58 | } 59 | 60 | func (o *Options) volumes() map[string]struct{} { 61 | volumes := make(map[string]struct{}) 62 | for _, v := range o.Volumes { 63 | volumes[v] = struct{}{} 64 | } 65 | return volumes 66 | } 67 | 68 | func (o *Options) env() []string { 69 | env := make([]string, 0, len(o.Env)) 70 | for k, v := range o.Env { 71 | env = append(env, k+"="+v) 72 | } 73 | return env 74 | } 75 | -------------------------------------------------------------------------------- /container_info_test.go: -------------------------------------------------------------------------------- 1 | package dktest_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | import ( 8 | "github.com/docker/go-connections/nat" 9 | ) 10 | 11 | import ( 12 | "github.com/dhui/dktest" 13 | ) 14 | 15 | func getTestContainerInfo(t *testing.T) dktest.ContainerInfo { 16 | _, portMap, err := nat.ParsePortSpecs([]string{"8080:80", "3737:37/udp"}) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | return dktest.ContainerInfo{ 22 | ID: "testID", 23 | Name: "testContainerInfo", 24 | ImageName: "testImageName", 25 | Ports: portMap, 26 | } 27 | } 28 | 29 | func TestContainerInfoString(t *testing.T) { 30 | // real test cases in internal tests for portMapToStrings() 31 | ci := getTestContainerInfo(t) 32 | expected := `dktest.ContainerInfo{ID:"testID", Name:"testContainerInfo", ImageName:"testImageName", Ports:[80/tcp -> :8080 37/udp -> :3737]}` 33 | if s := ci.String(); s != expected { 34 | t.Error("ContainerInfo String() doesn't match expected:", s, "!=", expected) 35 | } 36 | } 37 | 38 | // nolint:unparam 39 | func expectMapping(t *testing.T, ip, port string, err error, expectedIP, expectedPort string, expectedErr error) { 40 | if ip != expectedIP { 41 | t.Error("ip does not match expected:", ip, "!=", expectedIP) 42 | } 43 | if port != expectedPort { 44 | t.Error("port does not match expected:", port, "!=", expectedPort) 45 | } 46 | if err != expectedErr { 47 | t.Error("err does not match expected:", err, "!=", expectedErr) 48 | } 49 | } 50 | 51 | func TestContainerInfoPort(t *testing.T) { 52 | // real test cases in internal tests for mapPort() 53 | ci := getTestContainerInfo(t) 54 | ip, port, err := ci.Port(80) 55 | expectMapping(t, ip, port, err, "127.0.0.1", "8080", nil) 56 | } 57 | 58 | func TestContainerInfoUDPPort(t *testing.T) { 59 | // real test cases in internal tests for mapPort() 60 | ci := getTestContainerInfo(t) 61 | ip, port, err := ci.UDPPort(37) 62 | expectMapping(t, ip, port, err, "127.0.0.1", "3737", nil) 63 | } 64 | 65 | func TestContainerInfoFirstPort(t *testing.T) { 66 | // real test cases in internal tests for firstPort() 67 | ci := getTestContainerInfo(t) 68 | ip, port, err := ci.FirstPort() 69 | expectMapping(t, ip, port, err, "127.0.0.1", "8080", nil) 70 | } 71 | 72 | func TestContainerInfoFirstUDPPort(t *testing.T) { 73 | // real test cases in internal tests for firstPort() 74 | ci := getTestContainerInfo(t) 75 | ip, port, err := ci.FirstUDPPort() 76 | expectMapping(t, ip, port, err, "127.0.0.1", "3737", nil) 77 | } 78 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package dktest_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | import ( 13 | "github.com/dhui/dktest" 14 | _ "github.com/lib/pq" 15 | ) 16 | 17 | func Example_nginx() { 18 | dockerImageName := "nginx:alpine" 19 | readyFunc := func(ctx context.Context, c dktest.ContainerInfo) bool { 20 | ip, port, err := c.FirstPort() 21 | if err != nil { 22 | return false 23 | } 24 | u := url.URL{Scheme: "http", Host: ip + ":" + port} 25 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 26 | if err != nil { 27 | fmt.Println(err) 28 | return false 29 | } 30 | req = req.WithContext(ctx) 31 | if resp, err := http.DefaultClient.Do(req); err != nil { 32 | return false 33 | } else if resp.StatusCode != 200 { 34 | return false 35 | } 36 | return true 37 | } 38 | 39 | // dktest.Run() should be used within a test 40 | dktest.Run(&testing.T{}, dockerImageName, dktest.Options{PortRequired: true, ReadyFunc: readyFunc}, 41 | func(t *testing.T, c dktest.ContainerInfo) { // nolint:revive 42 | // test code here 43 | }) 44 | 45 | // Output: 46 | } 47 | 48 | func Example_postgres() { 49 | dockerImageName := "postgres:alpine" 50 | readyFunc := func(ctx context.Context, c dktest.ContainerInfo) bool { 51 | ip, port, err := c.FirstPort() 52 | if err != nil { 53 | return false 54 | } 55 | connStr := fmt.Sprintf("host=%s port=%s user=postgres password=password dbname=postgres sslmode=disable", ip, port) 56 | db, err := sql.Open("postgres", connStr) 57 | if err != nil { 58 | return false 59 | } 60 | defer db.Close() // nolint:errcheck 61 | return db.PingContext(ctx) == nil 62 | } 63 | 64 | // dktest.Run() should be used within a test 65 | dktest.Run(&testing.T{}, dockerImageName, dktest.Options{ 66 | PortRequired: true, 67 | ReadyFunc: readyFunc, 68 | Env: map[string]string{"POSTGRES_PASSWORD": "password"}}, 69 | func(t *testing.T, c dktest.ContainerInfo) { 70 | ip, port, err := c.FirstPort() 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | connStr := fmt.Sprintf("host=%s port=%s user=postgres password=password dbname=postgres sslmode=disable", ip, port) 75 | db, err := sql.Open("postgres", connStr) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | defer db.Close() // nolint:errcheck 80 | if err := db.Ping(); err != nil { 81 | t.Fatal(err) 82 | } 83 | // Test using db 84 | }) 85 | 86 | // Output: 87 | } 88 | -------------------------------------------------------------------------------- /dktest_test.go: -------------------------------------------------------------------------------- 1 | package dktest_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "testing" 10 | 11 | "github.com/dhui/dktest" 12 | "github.com/docker/go-connections/nat" 13 | ) 14 | 15 | const ( 16 | testImage = "alpine" 17 | testNetworkImage = "nginx:alpine" 18 | ) 19 | 20 | // ready functions 21 | func nginxReady(ctx context.Context, c dktest.ContainerInfo) bool { 22 | ip, port, err := c.FirstPort() 23 | if err != nil { 24 | return false 25 | } 26 | u := url.URL{Scheme: "http", Host: ip + ":" + port} 27 | fmt.Println(u.String()) 28 | req, err := http.NewRequest(http.MethodGet, u.String(), nil) 29 | if err != nil { 30 | fmt.Println("req err", err) 31 | return false 32 | } 33 | req = req.WithContext(ctx) 34 | if resp, err := http.DefaultClient.Do(req); err != nil { 35 | fmt.Println("do err:", err) 36 | return false 37 | } else if resp.StatusCode != 200 { 38 | return false 39 | } 40 | return true 41 | } 42 | 43 | // test functions 44 | func noop(*testing.T, dktest.ContainerInfo) {} 45 | 46 | func TestRun(t *testing.T) { 47 | dktest.Run(t, testImage, dktest.Options{}, noop) 48 | } 49 | 50 | func TestRunContext(t *testing.T) { 51 | t.Run("success", func(t *testing.T) { 52 | err := dktest.RunContext(context.Background(), t, testImage, dktest.Options{}, func(dktest.ContainerInfo) error { 53 | return nil 54 | }) 55 | if err != nil { 56 | t.Fatal("failed", err) 57 | } 58 | }) 59 | 60 | t.Run("test func returns error", func(t *testing.T) { 61 | var errForTest = errors.New("testFunc failed") 62 | err := dktest.RunContext(context.Background(), t, testImage, dktest.Options{}, func(dktest.ContainerInfo) error { 63 | return errForTest 64 | }) 65 | if err == nil { 66 | t.Fatal("expected error") 67 | } 68 | if errors.Unwrap(err) != errForTest { 69 | t.Fatal("test func error not propagated with cause, got error:", err) 70 | } 71 | }) 72 | } 73 | 74 | func TestRunParallel(t *testing.T) { 75 | numTests := 10 76 | for i := 0; i < numTests; i++ { 77 | t.Run("", func(t *testing.T) { 78 | t.Parallel() 79 | dktest.Run(t, testImage, dktest.Options{}, noop) 80 | }) 81 | } 82 | } 83 | 84 | func TestRunWithNetwork(t *testing.T) { 85 | dktest.Run(t, testNetworkImage, dktest.Options{ReadyFunc: nginxReady, PortRequired: true}, noop) 86 | } 87 | 88 | func TestRunWithNetworkPortBinding(t *testing.T) { 89 | port, err := nat.NewPort("tcp", "80") 90 | if err != nil { 91 | t.Fatal("Invalid port:", err) 92 | } 93 | 94 | dktest.Run(t, testNetworkImage, dktest.Options{ReadyFunc: nginxReady, PortRequired: true, 95 | PortBindings: nat.PortMap{port: []nat.PortBinding{{HostPort: "8181"}}}}, noop) 96 | } 97 | 98 | func TestRunWithExposesPorts(t *testing.T) { 99 | port, err := nat.NewPort("tcp", "8080") 100 | if err != nil { 101 | t.Fatal("Invalid port:", err) 102 | } 103 | 104 | dktest.Run(t, testNetworkImage, dktest.Options{ 105 | ReadyFunc: nginxReady, 106 | PortRequired: true, 107 | ExposedPorts: nat.PortSet{port: struct{}{}}, 108 | }, func(t *testing.T, info dktest.ContainerInfo) { 109 | if _, ok := info.Ports[port]; !ok { 110 | t.Fatal("port not exposed") 111 | } 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /container_info.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | import ( 9 | "github.com/docker/go-connections/nat" 10 | ) 11 | 12 | func mapHost(h string) string { 13 | switch h { 14 | case "", "0.0.0.0": 15 | return "127.0.0.1" 16 | default: 17 | return h 18 | } 19 | } 20 | 21 | func mapPort(portMap nat.PortMap, port nat.Port) (hostIP string, hostPort string, err error) { 22 | // Single port mapped 23 | portBindings, ok := portMap[port] 24 | if ok { 25 | for _, pb := range portBindings { 26 | return mapHost(pb.HostIP), pb.HostPort, nil 27 | } 28 | } 29 | 30 | // Search for port mapped in a range 31 | portInt := port.Int() 32 | proto := port.Proto() 33 | for p, portBindings := range portMap { 34 | if p.Proto() != proto { 35 | continue 36 | } 37 | start, end, err := p.Range() 38 | if err != nil { 39 | continue 40 | } 41 | if portInt < start || portInt > end { 42 | continue 43 | } 44 | offset := portInt - start 45 | if offset >= len(portBindings) { 46 | continue 47 | } 48 | pb := portBindings[offset] 49 | return mapHost(pb.HostIP), pb.HostPort, nil 50 | } 51 | 52 | return "", "", errNoPort 53 | } 54 | 55 | // firstPort gets the first port from the nat.PortMap. 56 | // Since the underlying type is a map, the first port returned will not be consistent 57 | func firstPort(portMap nat.PortMap, proto string) (hostIP string, hostPort string, err error) { 58 | for p, portBindings := range portMap { 59 | if p.Proto() != proto { 60 | continue 61 | } 62 | for _, pb := range portBindings { 63 | return mapHost(pb.HostIP), pb.HostPort, nil 64 | } 65 | } 66 | return "", "", errNoPort 67 | } 68 | 69 | func portMapToStrings(portMap nat.PortMap) []string { 70 | var portBindingStrs []string 71 | ports := make([]nat.Port, 0, len(portMap)) 72 | for p := range portMap { 73 | ports = append(ports, p) 74 | } 75 | 76 | nat.SortPortMap(ports, portMap) 77 | 78 | for _, p := range ports { 79 | start, end, err := p.Range() 80 | if err != nil { 81 | continue 82 | } 83 | 84 | portBindings, ok := portMap[p] 85 | if !ok { 86 | // Very unlikely to happen 87 | continue 88 | } 89 | 90 | l := min(end-start+1, len(portBindings)) 91 | proto := p.Proto() 92 | for i := 0; i < l; i++ { 93 | pb := portBindings[i] 94 | portBindingStrs = append(portBindingStrs, strconv.Itoa(start+i)+"/"+proto+" -> "+ 95 | pb.HostIP+":"+pb.HostPort) 96 | } 97 | } 98 | return portBindingStrs 99 | } 100 | 101 | // ContainerInfo holds information about a running Docker container 102 | type ContainerInfo struct { 103 | ID string 104 | Name string 105 | ImageName string 106 | Ports nat.PortMap 107 | } 108 | 109 | // String gets the string representation for the ContainerInfo. This is intended for debugging purposes. 110 | func (c ContainerInfo) String() string { 111 | return fmt.Sprintf("dktest.ContainerInfo{ID:%q, Name:%q, ImageName:%q, Ports:%v}", c.ID, c.Name, c.ImageName, 112 | portMapToStrings(c.Ports)) 113 | } 114 | 115 | // Port gets the specified published/bound/mapped TCP port 116 | func (c ContainerInfo) Port(containerPort uint16) (hostIP string, hostPort string, err error) { 117 | port, err := nat.NewPort("tcp", strconv.Itoa(int(containerPort))) 118 | if err != nil { 119 | return "", "", err 120 | } 121 | return mapPort(c.Ports, port) 122 | } 123 | 124 | // UDPPort gets the specified published/bound/mapped UDP port 125 | func (c ContainerInfo) UDPPort(containerPort uint16) (hostIP string, hostPort string, err error) { 126 | port, err := nat.NewPort("udp", strconv.Itoa(int(containerPort))) 127 | if err != nil { 128 | return "", "", err 129 | } 130 | return mapPort(c.Ports, port) 131 | } 132 | 133 | // FirstPort gets the first published/bound/mapped TCP port. It is always safer to use Port(). 134 | // This provided as a convenience method and should only be used with Docker images that only expose a single port. 135 | // If the Docker image exposes multiple ports, then the "first" port will not always be the same. 136 | func (c ContainerInfo) FirstPort() (hostIP string, hostPort string, err error) { 137 | return firstPort(c.Ports, "tcp") 138 | } 139 | 140 | // FirstUDPPort gets the first published/bound/mapped UDP port. It is always safer to use UDPPort(). 141 | // This provided as a convenience method and should only be used with Docker images that only expose a single port. 142 | // If the Docker image exposes multiple ports, then the "first" port will not always be the same. 143 | func (c ContainerInfo) FirstUDPPort() (hostIP string, hostPort string, err error) { 144 | return firstPort(c.Ports, "udp") 145 | } 146 | -------------------------------------------------------------------------------- /container_info_internal_test.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/docker/go-connections/nat" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMapHost(t *testing.T) { 12 | testCases := []struct { 13 | host string 14 | expectedMappedHost string 15 | }{ 16 | {host: "", expectedMappedHost: "127.0.0.1"}, 17 | {host: "0.0.0.0", expectedMappedHost: "127.0.0.1"}, 18 | {host: "0.0.0.1", expectedMappedHost: "0.0.0.1"}, 19 | {host: "localhost", expectedMappedHost: "localhost"}, 20 | {host: "not a host", expectedMappedHost: "not a host"}, 21 | } 22 | 23 | for _, tc := range testCases { 24 | t.Run(tc.host, func(t *testing.T) { 25 | if h := mapHost(tc.host); h != tc.expectedMappedHost { 26 | t.Error("mapped host does not match expected:", h, "!=", tc.expectedMappedHost) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func expectMapping(t *testing.T, ip, port string, err error, expectedIP, expectedPort string, expectedErr error) { 33 | if ip != expectedIP { 34 | t.Error("ip does not match expected:", ip, "!=", expectedIP) 35 | } 36 | if port != expectedPort { 37 | t.Error("port does not match expected:", port, "!=", expectedPort) 38 | } 39 | if err != expectedErr { 40 | t.Error("err does not match expected:", err, "!=", expectedErr) 41 | } 42 | } 43 | 44 | func TestMapPort(t *testing.T) { 45 | _, portMap, err := nat.ParsePortSpecs([]string{"9000:8000", "10000-11000:9000-10000"}) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | portBindingsForRange := make([]nat.PortBinding, 0, 1000) 50 | for i := 10000; i <= 11000; i++ { 51 | portBindingsForRange = append(portBindingsForRange, nat.PortBinding{HostPort: strconv.Itoa(i)}) 52 | } 53 | portMapWithRange := nat.PortMap{ 54 | "9000-10000": portBindingsForRange, 55 | } 56 | 57 | testCases := []struct { 58 | name string 59 | portMap nat.PortMap 60 | port nat.Port 61 | expectedIP string 62 | expectedPort string 63 | expectedErr error 64 | }{ 65 | {name: "invalid search port", portMap: portMap, port: "", expectedErr: errNoPort}, 66 | {name: "wrong protocol", portMap: portMap, port: "8000/udp", expectedErr: errNoPort}, 67 | {name: "success - single port", portMap: portMap, port: "8000/tcp", 68 | expectedIP: "127.0.0.1", expectedPort: "9000"}, 69 | {name: "port range - parsed", portMap: portMap, port: "9050/tcp", 70 | expectedIP: "127.0.0.1", expectedPort: "10050"}, 71 | {name: "port range - manual - success", portMap: portMapWithRange, port: "9050/tcp", 72 | expectedIP: "127.0.0.1", expectedPort: "10050"}, 73 | {name: "port range - manual - malformed range", portMap: nat.PortMap{"foobar": []nat.PortBinding{}}, 74 | port: "9050/tcp", expectedErr: errNoPort}, 75 | {name: "port range - manual - invalid range", portMap: nat.PortMap{"10000-9000": []nat.PortBinding{}}, 76 | port: "9050/tcp", expectedErr: errNoPort}, 77 | {name: "port range - manual - not in range", portMap: nat.PortMap{"2000-3000": []nat.PortBinding{}}, 78 | port: "9050/tcp", expectedErr: errNoPort}, 79 | {name: "port range - manual - invalid mapping", portMap: nat.PortMap{"9000-10000": []nat.PortBinding{}}, 80 | port: "9050/tcp", expectedErr: errNoPort}, 81 | } 82 | 83 | for _, tc := range testCases { 84 | t.Run(tc.name, func(t *testing.T) { 85 | ip, port, err := mapPort(tc.portMap, tc.port) 86 | expectMapping(t, ip, port, err, tc.expectedIP, tc.expectedPort, tc.expectedErr) 87 | }) 88 | } 89 | } 90 | 91 | func TestFirstPort(t *testing.T) { 92 | _, portMap, err := nat.ParsePortSpecs([]string{"9000:8000"}) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | testCases := []struct { 98 | name string 99 | portMap nat.PortMap 100 | proto string 101 | expectedIP string 102 | expectedPort string 103 | expectedErr error 104 | }{ 105 | {name: "invalid proto", portMap: portMap, proto: "", expectedErr: errNoPort}, 106 | {name: "wrong proto", portMap: portMap, proto: "udp", expectedErr: errNoPort}, 107 | {name: "success", portMap: portMap, proto: "tcp", expectedIP: "127.0.0.1", expectedPort: "9000"}, 108 | } 109 | 110 | for _, tc := range testCases { 111 | t.Run(tc.name, func(t *testing.T) { 112 | ip, port, err := firstPort(tc.portMap, tc.proto) 113 | expectMapping(t, ip, port, err, tc.expectedIP, tc.expectedPort, tc.expectedErr) 114 | }) 115 | } 116 | } 117 | 118 | func TestPortMapToStrings(t *testing.T) { 119 | _, portMap, err := nat.ParsePortSpecs([]string{"9000-9010:8000-8010", "8000:7000"}) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | testCases := []struct { 125 | name string 126 | portMap nat.PortMap 127 | expected []string 128 | }{ 129 | {name: "malformed port range", portMap: nat.PortMap{"foobar": []nat.PortBinding{}}}, 130 | {name: "success", portMap: portMap, expected: []string{ 131 | "8010/tcp -> :9010", 132 | "8009/tcp -> :9009", 133 | "8008/tcp -> :9008", 134 | "8007/tcp -> :9007", 135 | "8006/tcp -> :9006", 136 | "8005/tcp -> :9005", 137 | "8004/tcp -> :9004", 138 | "8003/tcp -> :9003", 139 | "8002/tcp -> :9002", 140 | "8001/tcp -> :9001", 141 | "8000/tcp -> :9000", 142 | "7000/tcp -> :8000", 143 | }}, 144 | } 145 | 146 | for _, tc := range testCases { 147 | t.Run(tc.name, func(t *testing.T) { 148 | s := portMapToStrings(tc.portMap) 149 | assert.Equal(t, tc.expected, s) 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /mockdockerclient/image_api_client.go: -------------------------------------------------------------------------------- 1 | package mockdockerclient 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/docker/docker/api/types/build" 8 | "github.com/docker/docker/api/types/filters" 9 | "github.com/docker/docker/api/types/image" 10 | "github.com/docker/docker/api/types/registry" 11 | "github.com/docker/docker/client" 12 | ) 13 | 14 | var _ client.ImageAPIClient = (*ImageAPIClient)(nil) 15 | 16 | // ImageAPIClient is a mock implementation of the Docker's client.ImageAPIClient interface 17 | type ImageAPIClient struct { 18 | PullResp io.ReadCloser 19 | } 20 | 21 | // ImageBuild is a mock implementation of Docker's client.ImageAPIClient.ImageBuild() 22 | // 23 | // TODO: properly implement 24 | func (c *ImageAPIClient) ImageBuild(context.Context, io.Reader, 25 | build.ImageBuildOptions) (build.ImageBuildResponse, error) { 26 | return build.ImageBuildResponse{}, nil 27 | } 28 | 29 | // BuildCachePrune is a mock implementation of Docker's client.ImageAPIClient.BuildCachePrune() 30 | // 31 | // TODO: properly implement 32 | func (c *ImageAPIClient) BuildCachePrune(context.Context, 33 | build.CachePruneOptions) (*build.CachePruneReport, error) { 34 | return nil, nil 35 | } 36 | 37 | // BuildCancel is a mock implementation of Docker's client.ImageAPIClient.BuildCancel() 38 | // 39 | // TODO: properly implement 40 | func (c *ImageAPIClient) BuildCancel(context.Context, string) error { return nil } 41 | 42 | // ImageCreate is a mock implementation of Docker's client.ImageAPIClient.ImageCreate() 43 | // 44 | // TODO: properly implement 45 | func (c *ImageAPIClient) ImageCreate(context.Context, string, 46 | image.CreateOptions) (io.ReadCloser, error) { 47 | return nil, nil 48 | } 49 | 50 | // ImageHistory is a mock implementation of Docker's client.ImageAPIClient.ImageHistory() 51 | // 52 | // TODO: properly implement 53 | func (c *ImageAPIClient) ImageHistory(context.Context, string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { 54 | return nil, nil 55 | } 56 | 57 | // ImageImport is a mock implementation of Docker's client.ImageAPIClient.ImageImport() 58 | // 59 | // TODO: properly implement 60 | func (c *ImageAPIClient) ImageImport(context.Context, image.ImportSource, string, 61 | image.ImportOptions) (io.ReadCloser, error) { 62 | return nil, nil 63 | } 64 | 65 | // ImageInspectWithRaw is a mock implementation of Docker's client.ImageAPIClient.ImageInspectWithRaw() 66 | // 67 | // TODO: properly implement 68 | func (c *ImageAPIClient) ImageInspectWithRaw(context.Context, string) (image.InspectResponse, []byte, error) { 69 | return image.InspectResponse{}, nil, nil 70 | } 71 | 72 | // ImageList is a mock implementation of Docker's client.ImageAPIClient.ImageList() 73 | // 74 | // TODO: properly implement 75 | func (c *ImageAPIClient) ImageList(context.Context, image.ListOptions) ([]image.Summary, error) { 76 | return nil, nil 77 | } 78 | 79 | // ImageLoad is a mock implementation of Docker's client.ImageAPIClient.ImageLoad() 80 | // 81 | // TODO: properly implement 82 | func (c *ImageAPIClient) ImageLoad(context.Context, io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) { 83 | return image.LoadResponse{}, nil 84 | } 85 | 86 | // ImagePull is a mock implementation of Docker's client.ImageAPIClient.ImagePull() 87 | func (c *ImageAPIClient) ImagePull(context.Context, string, image.PullOptions) (io.ReadCloser, error) { 88 | if c.PullResp == nil { 89 | return nil, Err 90 | } 91 | return c.PullResp, nil 92 | } 93 | 94 | // ImagePush is a mock implementation of Docker's client.ImageAPIClient.ImagePush() 95 | // 96 | // TODO: properly implement 97 | func (c *ImageAPIClient) ImagePush(context.Context, string, image.PushOptions) (io.ReadCloser, error) { 98 | return nil, nil 99 | } 100 | 101 | // ImageRemove is a mock implementation of Docker's client.ImageAPIClient.ImageRemove() 102 | // 103 | // TODO: properly implement 104 | func (c *ImageAPIClient) ImageRemove(context.Context, string, 105 | image.RemoveOptions) ([]image.DeleteResponse, error) { 106 | return nil, nil 107 | } 108 | 109 | // ImageSearch is a mock implementation of Docker's client.ImageAPIClient.ImageSearch() 110 | // 111 | // TODO: properly implement 112 | func (c *ImageAPIClient) ImageSearch(context.Context, string, 113 | registry.SearchOptions) ([]registry.SearchResult, error) { 114 | return nil, nil 115 | } 116 | 117 | // ImageSave is a mock implementation of Docker's client.ImageAPIClient.ImageSave() 118 | // 119 | // TODO: properly implement 120 | func (c *ImageAPIClient) ImageSave(context.Context, []string, ...client.ImageSaveOption) (io.ReadCloser, error) { 121 | return nil, nil 122 | } 123 | 124 | // ImageTag is a mock implementation of Docker's client.ImageAPIClient.ImageTag() 125 | // 126 | // TODO: properly implement 127 | func (c *ImageAPIClient) ImageTag(context.Context, string, string) error { return nil } 128 | 129 | // ImagesPrune is a mock implementation of Docker's client.ImageAPIClient.ImagesPrune() 130 | // 131 | // TODO: properly implement 132 | func (c *ImageAPIClient) ImagesPrune(context.Context, filters.Args) (image.PruneReport, error) { 133 | return image.PruneReport{}, nil 134 | } 135 | 136 | // ImageInspect is a mock implementation of Docker's client.ImageAPIClient.ImageInspect() 137 | // 138 | // TODO: properly implement 139 | func (c *ImageAPIClient) ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error) { 140 | return image.InspectResponse{}, nil 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dktest 2 | 3 | ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/dhui/dktest/go.yml?branch=master) [![Coverage Status](https://img.shields.io/coveralls/github/dhui/dktest/master.svg)](https://coveralls.io/github/dhui/dktest?branch=master) [![GoDoc](https://godoc.org/github.com/dhui/dktest?status.svg)](https://godoc.org/github.com/dhui/dktest) [![Go Report Card](https://goreportcard.com/badge/github.com/dhui/dktest)](https://goreportcard.com/report/github.com/dhui/dktest) [![GitHub Release](https://img.shields.io/github/release/dhui/dktest/all.svg)](https://github.com/dhui/dktest/releases) ![Supported Go versions](https://img.shields.io/badge/Go-1.24%2C%201.25-lightgrey.svg) 4 | 5 | `dktest` is short for **d**oc**k**er**test**. 6 | 7 | `dktest` makes it stupidly easy to write integration tests in Go using Docker. Pulling images, starting containers, and cleaning up (even if your tests panic) is handled for you automatically! 8 | 9 | ## API 10 | 11 | `Run()` is the workhorse 12 | 13 | ```golang 14 | type ContainerInfo struct { 15 | ID string 16 | Name string 17 | ImageName string 18 | IP string 19 | Port string 20 | } 21 | 22 | type Options struct { 23 | Timeout time.Duration 24 | ReadyFunc func(ContainerInfo) bool 25 | Env map[string]string 26 | // If you prefer to specify your port bindings as a string, use nat.ParsePortSpecs() 27 | PortBindings nat.PortMap 28 | PortRequired bool 29 | } 30 | 31 | func Run(t *testing.T, imgName string, opts Options, testFunc func(*testing.T, ContainerInfo)) 32 | ``` 33 | 34 | ## Example Usage 35 | 36 | ```golang 37 | import ( 38 | "context" 39 | "testing" 40 | ) 41 | 42 | import ( 43 | "github.com/dhui/dktest" 44 | _ "github.com/lib/pq" 45 | ) 46 | 47 | func pgReady(ctx context.Context, c dktest.ContainerInfo) bool { 48 | ip, port, err := c.FirstPort() 49 | if err != nil { 50 | return false 51 | } 52 | connStr := fmt.Sprintf("host=%s port=%s user=postgres dbname=postgres sslmode=disable", ip, port) 53 | db, err := sql.Open("postgres", connStr) 54 | if err != nil { 55 | return false 56 | } 57 | defer db.Close() 58 | return db.PingContext(ctx) == nil 59 | } 60 | 61 | func Test(t *testing.T) { 62 | dktest.Run(t, "postgres:alpine", dktest.Options{PortRequired: true, ReadyFunc: pgReady}, 63 | func(t *testing.T, c dktest.ContainerInfo) { 64 | ip, port, err := c.FirstPort() 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | connStr := fmt.Sprintf("host=%s port=%s user=postgres dbname=postgres sslmode=disable", ip, port) 69 | db, err := sql.Open("postgres", connStr) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | defer db.Close() 74 | if err := db.Ping(); err != nil { 75 | t.Fatal(err) 76 | } 77 | // Test using db 78 | }) 79 | } 80 | ``` 81 | 82 | For more examples, see the [docs](https://godoc.org/github.com/dhui/dktest). 83 | 84 | ## Debugging tests 85 | 86 | Running `go test` with the `-v` option will display the container lifecycle log statements 87 | along with the container ID. 88 | 89 | ### Short lived tests/containers 90 | 91 | Run `go test` with the `-v` option and specify the `LogStdout` and/or `LogStderr` `Options` 92 | to see the container's logs. 93 | 94 | ### Interactive tests/containers 95 | 96 | Run `go test` with the `-v` option to get the container ID and check the container's logs with 97 | `docker logs -f $CONTAINER_ID`. 98 | 99 | ## Cleaning up dangling containers 100 | 101 | In the unlikely scenario where `dktest` leaves dangling containers, 102 | you can find and removing them by using the `dktest` label: 103 | 104 | ```shell 105 | # list dangling containers 106 | $ docker ps -a --filter label=dktest 107 | # stop dangling containers 108 | $ docker ps --filter label=dktest | awk '{print $1}' | grep -v CONTAINER | xargs docker stop 109 | # remove dangling containers 110 | $ docker container prune --filter label=dktest 111 | ``` 112 | 113 | ## Roadmap 114 | 115 | * [x] Support multiple ports in `ContainerInfo` 116 | * [ ] Use non-default network 117 | * [ ] Add more `Options` 118 | * [x] Volume mounts 119 | * [ ] Network config 120 | * [ ] Support testing against multiple containers. It can be faked for now by nested/recursive `Run()` calls but that serializes the containers' startup time. 121 | 122 | ## Comparisons 123 | 124 | Last updated: 2020/01/03 125 | 126 | ### [dockertest](https://github.com/ory/dockertest) 127 | 128 | #### Why `dktest` is better 129 | 130 | * Uses the [official Docker SDK](https://github.com/docker/docker) 131 | * [docker/docker](https://github.com/docker/docker) (aka [moby/moby](https://github.com/moby/moby)) uses [import path checking](https://golang.org/cmd/go/#hdr-Import_path_checking), so needs to be imported as `github.com/docker/docker` 132 | * Designed to run in the Go testing environment 133 | * Smaller API surface 134 | * Running Docker containers are automatically cleaned up 135 | * Has better test coverage 136 | * Uses package management (Go modules) properly. e.g. not [manually vendored](https://github.com/ory/dockertest/pull/122) 137 | 138 | #### Why `dockertest` is better 139 | 140 | * Has been around longer and API is more stable 141 | * More options for configuring Docker containers 142 | * Has more Github stars and contributors 143 | 144 | ### [testcontainers-go](https://github.com/testcontainers/testcontainers-go) 145 | 146 | TBD 147 | -------------------------------------------------------------------------------- /dktest.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/docker/docker/api/types/container" 12 | "github.com/docker/docker/api/types/image" 13 | "github.com/docker/docker/api/types/network" 14 | "github.com/docker/docker/client" 15 | "github.com/docker/docker/pkg/jsonmessage" 16 | ) 17 | 18 | var ( 19 | // DefaultPullTimeout is the default timeout used when pulling images 20 | DefaultPullTimeout = time.Minute 21 | // DefaultTimeout is the default timeout used when starting a container and checking if it's ready 22 | DefaultTimeout = time.Minute 23 | // DefaultReadyTimeout is the default timeout used for each container ready check. 24 | // e.g. each invocation of the ReadyFunc 25 | DefaultReadyTimeout = 2 * time.Second 26 | // DefaultCleanupTimeout is the default timeout used when stopping and removing a container 27 | DefaultCleanupTimeout = 15 * time.Second 28 | ) 29 | 30 | const ( 31 | label = "dktest" 32 | ) 33 | 34 | func pullImage(ctx context.Context, lgr Logger, dc client.ImageAPIClient, registryAuth, imgName, platform string) error { 35 | lgr.Log("Pulling image:", imgName) 36 | // lgr.Log(dc.ImageList(ctx, types.ImageListOptions{All: true})) 37 | 38 | resp, err := dc.ImagePull(ctx, imgName, image.PullOptions{ 39 | Platform: platform, 40 | RegistryAuth: registryAuth, 41 | }) 42 | if err != nil { 43 | return err 44 | } 45 | defer func() { 46 | if err := resp.Close(); err != nil { 47 | lgr.Log("Failed to close image response:", err) 48 | } 49 | }() 50 | 51 | // Log response 52 | b := strings.Builder{} 53 | if err := jsonmessage.DisplayJSONMessagesStream(resp, &b, 0, false, nil); err == nil { 54 | lgr.Log("Image pull response:", b.String()) 55 | } else { 56 | lgr.Log("Error parsing image pull response:", err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func removeImage(ctx context.Context, lgr Logger, dc client.ImageAPIClient, imgName string) { 63 | lgr.Log("Removing image:", imgName) 64 | 65 | if _, err := dc.ImageRemove(ctx, imgName, image.RemoveOptions{Force: true, PruneChildren: true}); err != nil { 66 | lgr.Log("Failed to remove image: ", err.Error()) 67 | } 68 | } 69 | 70 | func runImage(ctx context.Context, lgr Logger, dc client.ContainerAPIClient, imgName string, 71 | opts Options) (ContainerInfo, error) { 72 | c := ContainerInfo{Name: genContainerName(), ImageName: imgName} 73 | createResp, err := dc.ContainerCreate(ctx, &container.Config{ 74 | Image: imgName, 75 | Labels: map[string]string{label: "true"}, 76 | Env: opts.env(), 77 | Entrypoint: opts.Entrypoint, 78 | Cmd: opts.Cmd, 79 | Volumes: opts.volumes(), 80 | Hostname: opts.Hostname, 81 | ExposedPorts: opts.ExposedPorts, 82 | }, &container.HostConfig{ 83 | PublishAllPorts: true, 84 | PortBindings: opts.PortBindings, 85 | ShmSize: opts.ShmSize, 86 | Mounts: opts.Mounts, 87 | }, &network.NetworkingConfig{}, 88 | nil, 89 | c.Name) 90 | if err != nil { 91 | return c, err 92 | } 93 | c.ID = createResp.ID 94 | lgr.Log("Created container:", c.String()) 95 | 96 | if err := dc.ContainerStart(ctx, createResp.ID, container.StartOptions{}); err != nil { 97 | return c, err 98 | } 99 | lgr.Log("Started container:", c.String()) 100 | 101 | if !opts.PortRequired { 102 | return c, nil 103 | } 104 | 105 | inspectResp, err := dc.ContainerInspect(ctx, c.ID) 106 | if err != nil { 107 | return c, err 108 | } 109 | lgr.Log("Inspected container:", c.String()) 110 | 111 | if inspectResp.NetworkSettings == nil { 112 | return c, errNoNetworkSettings 113 | } 114 | c.Ports = inspectResp.NetworkSettings.Ports 115 | 116 | return c, nil 117 | } 118 | 119 | func stopContainer(ctx context.Context, lgr Logger, dc client.ContainerAPIClient, c ContainerInfo, 120 | logStdout, logStderr bool) { 121 | if logStdout || logStderr { 122 | if logs, err := dc.ContainerLogs(ctx, c.ID, container.LogsOptions{ 123 | Timestamps: true, ShowStdout: logStdout, ShowStderr: logStderr, 124 | }); err == nil { 125 | b, err := io.ReadAll(logs) 126 | defer func() { 127 | if err := logs.Close(); err != nil { 128 | lgr.Log("Error closing logs:", err) 129 | } 130 | }() 131 | if err == nil { 132 | lgr.Log("Container logs:", string(b)) 133 | } else { 134 | lgr.Log("Error reading container logs:", err) 135 | } 136 | } else { 137 | lgr.Log("Error fetching container logs:", err) 138 | } 139 | } 140 | 141 | if err := dc.ContainerStop(ctx, c.ID, container.StopOptions{}); err != nil { 142 | lgr.Log("Error stopping container:", c.String(), "error:", err) 143 | } 144 | lgr.Log("Stopped container:", c.String()) 145 | 146 | if err := dc.ContainerRemove(ctx, c.ID, 147 | container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil { 148 | lgr.Log("Error removing container:", c.String(), "error:", err) 149 | } 150 | lgr.Log("Removed container:", c.String()) 151 | } 152 | 153 | func waitContainerReady(ctx context.Context, lgr Logger, c ContainerInfo, 154 | readyFunc func(context.Context, ContainerInfo) bool, readyTimeout time.Duration) bool { 155 | if readyFunc == nil { 156 | return true 157 | } 158 | 159 | ticker := time.NewTicker(time.Second) 160 | defer ticker.Stop() 161 | 162 | for { 163 | select { 164 | case <-ticker.C: 165 | ready := func() bool { 166 | readyCtx, canceledFunc := context.WithTimeout(ctx, readyTimeout) 167 | defer canceledFunc() 168 | return readyFunc(readyCtx, c) 169 | }() 170 | 171 | if ready { 172 | return true 173 | } 174 | case <-ctx.Done(): 175 | lgr.Log("Container was never ready:", c.String()) 176 | return false 177 | } 178 | } 179 | } 180 | 181 | // Run runs the given test function once the specified Docker image is running in a container 182 | func Run(t *testing.T, imgName string, opts Options, testFunc func(*testing.T, ContainerInfo)) { 183 | err := RunContext(context.Background(), t, imgName, opts, func(containerInfo ContainerInfo) error { 184 | testFunc(t, containerInfo) 185 | return nil 186 | }) 187 | if err != nil { 188 | t.Fatal("Failed:", err) 189 | } 190 | } 191 | 192 | // RunContext is similar to Run, but takes a parent context and returns an error and doesn't rely on a testing.T. 193 | func RunContext(ctx context.Context, logger Logger, imgName string, opts Options, testFunc func(ContainerInfo) error) (retErr error) { 194 | dc, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.41")) 195 | if err != nil { 196 | return fmt.Errorf("error getting Docker client: %w", err) 197 | } 198 | defer func() { 199 | if err := dc.Close(); err != nil && retErr == nil { 200 | retErr = fmt.Errorf("error closing Docker client: %w", err) 201 | } 202 | }() 203 | 204 | opts.init() 205 | pullCtx, pullTimeoutCancelFunc := context.WithTimeout(ctx, opts.PullTimeout) 206 | defer pullTimeoutCancelFunc() 207 | 208 | if err := pullImage(pullCtx, logger, dc, opts.PullRegistryAuth, imgName, opts.Platform); err != nil { 209 | return fmt.Errorf("error pulling image: %v error: %w", imgName, err) 210 | } 211 | 212 | return func() error { 213 | runCtx, runTimeoutCancelFunc := context.WithTimeout(ctx, opts.Timeout) 214 | defer runTimeoutCancelFunc() 215 | 216 | c, err := runImage(runCtx, logger, dc, imgName, opts) 217 | if err != nil { 218 | return fmt.Errorf("error running image: %v error: %w", imgName, err) 219 | } 220 | defer func() { 221 | stopCtx, stopTimeoutCancelFunc := context.WithTimeout(ctx, opts.CleanupTimeout) 222 | defer stopTimeoutCancelFunc() 223 | stopContainer(stopCtx, logger, dc, c, opts.LogStdout, opts.LogStderr) 224 | if opts.CleanupImage { 225 | removeImage(stopCtx, logger, dc, imgName) 226 | } 227 | }() 228 | 229 | if waitContainerReady(runCtx, logger, c, opts.ReadyFunc, opts.ReadyTimeout) { 230 | if err := testFunc(c); err != nil { 231 | return fmt.Errorf("error running test func: %w", err) 232 | } 233 | } else { 234 | return fmt.Errorf("timed out waiting for container to get ready: %v", c.String()) 235 | } 236 | 237 | return nil 238 | }() 239 | } 240 | -------------------------------------------------------------------------------- /dktest_internal_test.go: -------------------------------------------------------------------------------- 1 | package dktest 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | "time" 8 | 9 | "github.com/dhui/dktest/mockdockerclient" 10 | "github.com/docker/docker/api/types/container" 11 | "github.com/docker/go-connections/nat" 12 | ) 13 | 14 | const ( 15 | imageName = "dktestFakeImageName" 16 | ) 17 | 18 | var ( 19 | containerInfo = ContainerInfo{} 20 | ) 21 | 22 | // ready functions 23 | func alwaysReady(context.Context, ContainerInfo) bool { return true } 24 | func neverReady(context.Context, ContainerInfo) bool { return false } 25 | 26 | func testErr(t *testing.T, err error, expectErr bool) { 27 | t.Helper() 28 | if err == nil && expectErr { 29 | t.Error("Expected an error but didn't get one") 30 | } else if err != nil && !expectErr { 31 | t.Error("Got unexpected error:", err) 32 | } 33 | } 34 | 35 | func TestPullImage(t *testing.T) { 36 | successReader := mockdockerclient.MockReader{Err: io.EOF} 37 | 38 | testCases := []struct { 39 | name string 40 | client mockdockerclient.ImageAPIClient 41 | platform string 42 | expectErr bool 43 | }{ 44 | {name: "success", client: mockdockerclient.ImageAPIClient{ 45 | PullResp: mockdockerclient.MockReadCloser{MockReader: successReader}}, expectErr: false}, 46 | {name: "with specific platform", client: mockdockerclient.ImageAPIClient{ 47 | PullResp: mockdockerclient.MockReadCloser{MockReader: successReader}}, 48 | platform: "linux/x86_64", expectErr: false}, 49 | {name: "pull error", client: mockdockerclient.ImageAPIClient{}, expectErr: true}, 50 | {name: "read error", client: mockdockerclient.ImageAPIClient{ 51 | PullResp: mockdockerclient.MockReadCloser{ 52 | MockReader: mockdockerclient.MockReader{Err: mockdockerclient.Err}, 53 | }}, expectErr: false}, 54 | {name: "close error", client: mockdockerclient.ImageAPIClient{ 55 | PullResp: mockdockerclient.MockReadCloser{ 56 | MockReader: successReader, 57 | MockCloser: mockdockerclient.MockCloser{Err: mockdockerclient.Err}, 58 | }}, expectErr: false}, 59 | } 60 | 61 | ctx := context.Background() 62 | for _, tc := range testCases { 63 | t.Run(tc.name, func(t *testing.T) { 64 | client := tc.client 65 | err := pullImage(ctx, t, &client, "", imageName, tc.platform) 66 | testErr(t, err, tc.expectErr) 67 | }) 68 | } 69 | } 70 | 71 | func TestRunImage(t *testing.T) { 72 | _, portBindingsNoIP, err := nat.ParsePortSpecs([]string{"8181:80"}) 73 | if err != nil { 74 | t.Fatal("Error parsing port bindings:", err) 75 | } 76 | _, portBindingsIPZeros, err := nat.ParsePortSpecs([]string{"0.0.0.0:8181:80"}) 77 | if err != nil { 78 | t.Fatal("Error parsing port bindings:", err) 79 | } 80 | _, portBindingsDiffIP, err := nat.ParsePortSpecs([]string{"10.0.0.1:8181:80"}) 81 | if err != nil { 82 | t.Fatal("Error parsing port bindings:", err) 83 | } 84 | 85 | successCreateResp := &container.CreateResponse{} 86 | successInspectResp := &container.InspectResponse{} 87 | successInspectRespWithPortBindingNoIP := &container.InspectResponse{NetworkSettings: &container.NetworkSettings{ 88 | NetworkSettingsBase: container.NetworkSettingsBase{Ports: portBindingsNoIP}, 89 | }} 90 | successInspectRespWithPortBindingIPZeros := &container.InspectResponse{NetworkSettings: &container.NetworkSettings{ 91 | NetworkSettingsBase: container.NetworkSettingsBase{Ports: portBindingsIPZeros}, 92 | }} 93 | successInspectRespWithPortBindingDiffIP := &container.InspectResponse{NetworkSettings: &container.NetworkSettings{ 94 | NetworkSettingsBase: container.NetworkSettingsBase{Ports: portBindingsDiffIP}, 95 | }} 96 | 97 | testCases := []struct { 98 | name string 99 | client mockdockerclient.ContainerAPIClient 100 | opts Options 101 | expectErr bool 102 | }{ 103 | {name: "success", client: mockdockerclient.ContainerAPIClient{ 104 | CreateResp: successCreateResp, InspectResp: successInspectResp}, expectErr: false}, 105 | {name: "success - with port binding no ip", client: mockdockerclient.ContainerAPIClient{ 106 | CreateResp: successCreateResp, InspectResp: successInspectRespWithPortBindingNoIP}, expectErr: false}, 107 | {name: "success - with port binding ip 0.0.0.0", client: mockdockerclient.ContainerAPIClient{ 108 | CreateResp: successCreateResp, InspectResp: successInspectRespWithPortBindingIPZeros}, expectErr: false}, 109 | {name: "success - with port binding diff ip", client: mockdockerclient.ContainerAPIClient{ 110 | CreateResp: successCreateResp, InspectResp: successInspectRespWithPortBindingDiffIP}, expectErr: false}, 111 | {name: "create error", client: mockdockerclient.ContainerAPIClient{InspectResp: successInspectResp}, 112 | expectErr: true}, 113 | {name: "start error", client: mockdockerclient.ContainerAPIClient{ 114 | CreateResp: successCreateResp, StartErr: mockdockerclient.Err, InspectResp: successInspectResp, 115 | }, expectErr: true}, 116 | {name: "inspect error", client: mockdockerclient.ContainerAPIClient{CreateResp: successCreateResp}, 117 | opts: Options{PortRequired: true}, expectErr: true}, 118 | {name: "no network settings error", client: mockdockerclient.ContainerAPIClient{ 119 | CreateResp: successCreateResp, InspectResp: successInspectResp}, opts: Options{PortRequired: true}, 120 | expectErr: true}, 121 | } 122 | 123 | ctx := context.Background() 124 | for _, tc := range testCases { 125 | t.Run(tc.name, func(t *testing.T) { 126 | client := tc.client 127 | _, err := runImage(ctx, t, &client, imageName, tc.opts) 128 | testErr(t, err, tc.expectErr) 129 | }) 130 | } 131 | } 132 | 133 | func TestStopContainer(t *testing.T) { 134 | successReadCloser := mockdockerclient.MockReadCloser{MockReader: mockdockerclient.MockReader{Err: io.EOF}} 135 | readCloserReadErr := mockdockerclient.MockReadCloser{ 136 | MockReader: mockdockerclient.MockReader{Err: mockdockerclient.Err}} 137 | readCloserCloseErr := mockdockerclient.MockReadCloser{ 138 | MockReader: mockdockerclient.MockReader{Err: io.EOF}, 139 | MockCloser: mockdockerclient.MockCloser{Err: mockdockerclient.Err}} 140 | 141 | testCases := []struct { 142 | name string 143 | client mockdockerclient.ContainerAPIClient 144 | log bool 145 | }{ 146 | {name: "success", client: mockdockerclient.ContainerAPIClient{}}, 147 | {name: "success - log fetch error", client: mockdockerclient.ContainerAPIClient{}, log: true}, 148 | {name: "success - log fetch success - read error", 149 | client: mockdockerclient.ContainerAPIClient{Logs: readCloserReadErr}, log: true}, 150 | {name: "success - log fetch success - read success", 151 | client: mockdockerclient.ContainerAPIClient{Logs: successReadCloser}, log: true}, 152 | {name: "success - log fetch success - close error", 153 | client: mockdockerclient.ContainerAPIClient{Logs: readCloserCloseErr}, log: true}, 154 | {name: "stop error", client: mockdockerclient.ContainerAPIClient{StopErr: mockdockerclient.Err}}, 155 | {name: "remove error", client: mockdockerclient.ContainerAPIClient{RemoveErr: mockdockerclient.Err}}, 156 | } 157 | 158 | ctx := context.Background() 159 | for _, tc := range testCases { 160 | t.Run(tc.name, func(t *testing.T) { 161 | client := tc.client 162 | stopContainer(ctx, t, &client, containerInfo, tc.log, tc.log) 163 | }) 164 | } 165 | } 166 | 167 | func TestWaitContainerReady(t *testing.T) { 168 | canceledCtx, cancelFunc := context.WithCancel(context.Background()) 169 | cancelFunc() 170 | 171 | testCases := []struct { 172 | name string 173 | ctx context.Context 174 | readyFunc func(context.Context, ContainerInfo) bool 175 | expectReady bool 176 | }{ 177 | {name: "nil readyFunc", ctx: canceledCtx, readyFunc: nil, expectReady: true}, 178 | {name: "ready", ctx: context.Background(), readyFunc: alwaysReady, expectReady: true}, 179 | {name: "not ready", ctx: canceledCtx, readyFunc: neverReady, expectReady: false}, 180 | } 181 | 182 | for _, tc := range testCases { 183 | t.Run(tc.name, func(t *testing.T) { 184 | if ready := waitContainerReady(tc.ctx, t, containerInfo, tc.readyFunc, 185 | time.Second); ready && !tc.expectReady { 186 | t.Error("Expected container to not be ready but it was") 187 | } else if !ready && tc.expectReady { 188 | t.Error("Expected container to ready but it wasn't") 189 | } 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /mockdockerclient/container_api_client.go: -------------------------------------------------------------------------------- 1 | package mockdockerclient 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/docker/docker/api/types" 8 | "github.com/docker/docker/api/types/container" 9 | "github.com/docker/docker/api/types/filters" 10 | "github.com/docker/docker/api/types/network" 11 | "github.com/docker/docker/client" 12 | 13 | v1 "github.com/opencontainers/image-spec/specs-go/v1" 14 | ) 15 | 16 | // ContainerAPIClient is a mock implementation of the Docker's client.ContainerAPIClient interface 17 | type ContainerAPIClient struct { 18 | CreateResp *container.CreateResponse 19 | StartErr error 20 | StopErr error 21 | RemoveErr error 22 | InspectResp *container.InspectResponse 23 | Logs io.ReadCloser 24 | } 25 | 26 | var _ client.ContainerAPIClient = (*ContainerAPIClient)(nil) 27 | 28 | // ContainerAttach is a mock implementation of Docker's client.ContainerAPIClient.ContainerAttach() 29 | // 30 | // TODO: properly implement 31 | func (c *ContainerAPIClient) ContainerAttach(context.Context, string, 32 | container.AttachOptions) (types.HijackedResponse, error) { 33 | return types.HijackedResponse{}, nil 34 | } 35 | 36 | // ContainerCommit is a mock implementation of Docker's client.ContainerAPIClient.ContainerCommit() 37 | // 38 | // TODO: properly implement 39 | func (c *ContainerAPIClient) ContainerCommit(context.Context, string, 40 | container.CommitOptions) (container.CommitResponse, error) { 41 | return container.CommitResponse{}, nil 42 | } 43 | 44 | // ContainerCreate is a mock implementation of Docker's client.ContainerAPIClient.ContainerCreate() 45 | func (c *ContainerAPIClient) ContainerCreate(context.Context, *container.Config, *container.HostConfig, 46 | *network.NetworkingConfig, *v1.Platform, string) (container.CreateResponse, error) { 47 | if c.CreateResp == nil { 48 | return container.CreateResponse{}, Err 49 | } 50 | return *c.CreateResp, nil 51 | } 52 | 53 | // ContainerDiff is a mock implementation of Docker's client.ContainerAPIClient.ContainerDiff() 54 | // 55 | // TODO: properly implement 56 | func (c *ContainerAPIClient) ContainerDiff(context.Context, 57 | string) ([]container.FilesystemChange, error) { 58 | return nil, nil 59 | } 60 | 61 | // ContainerExecAttach is a mock implementation of Docker's client.ContainerAPIClient.ContainerExecAttach() 62 | // 63 | // TODO: properly implement 64 | func (c *ContainerAPIClient) ContainerExecAttach(context.Context, string, 65 | container.ExecStartOptions) (types.HijackedResponse, error) { 66 | return types.HijackedResponse{}, nil 67 | } 68 | 69 | // ContainerExecCreate is a mock implementation of Docker's client.ContainerAPIClient.ContainerExecCreate() 70 | // 71 | // TODO: properly implement 72 | func (c *ContainerAPIClient) ContainerExecCreate(context.Context, string, 73 | container.ExecOptions) (container.ExecCreateResponse, error) { 74 | return container.ExecCreateResponse{}, nil 75 | } 76 | 77 | // ContainerExecInspect is a mock implementation of Docker's client.ContainerAPIClient.ContainerExecInspect() 78 | // 79 | // TODO: properly implement 80 | func (c *ContainerAPIClient) ContainerExecInspect(context.Context, 81 | string) (container.ExecInspect, error) { 82 | return container.ExecInspect{}, nil 83 | } 84 | 85 | // ContainerExecResize is a mock implementation of Docker's client.ContainerAPIClient.ContainerExecResize() 86 | // 87 | // TODO: properly implement 88 | func (c *ContainerAPIClient) ContainerExecResize(context.Context, string, 89 | container.ResizeOptions) error { 90 | return nil 91 | } 92 | 93 | // ContainerExecStart is a mock implementation of Docker's client.ContainerAPIClient.ContainerExecStart() 94 | // 95 | // TODO: properly implement 96 | func (c *ContainerAPIClient) ContainerExecStart(context.Context, string, 97 | container.ExecStartOptions) error { 98 | return nil 99 | } 100 | 101 | // ContainerExport is a mock implementation of Docker's client.ContainerAPIClient.ContainerExport() 102 | // 103 | // TODO: properly implement 104 | func (c *ContainerAPIClient) ContainerExport(context.Context, string) (io.ReadCloser, error) { 105 | return nil, nil 106 | } 107 | 108 | // ContainerInspect is a mock implementation of Docker's client.ContainerAPIClient.ContainerInspect() 109 | func (c *ContainerAPIClient) ContainerInspect(context.Context, string) (container.InspectResponse, error) { 110 | if c.InspectResp == nil { 111 | return container.InspectResponse{}, Err 112 | } 113 | return *c.InspectResp, nil 114 | } 115 | 116 | // ContainerInspectWithRaw is a mock implementation of Docker's client.ContainerAPIClient.ContainerInspectWithRaw() 117 | // 118 | // TODO: properly implement 119 | func (c *ContainerAPIClient) ContainerInspectWithRaw(context.Context, string, 120 | bool) (container.InspectResponse, []byte, error) { 121 | return container.InspectResponse{}, nil, nil 122 | } 123 | 124 | // ContainerKill is a mock implementation of Docker's client.ContainerAPIClient.ContainerKill() 125 | // 126 | // TODO: properly implement 127 | func (c *ContainerAPIClient) ContainerKill(context.Context, string, string) error { 128 | return nil 129 | } 130 | 131 | // ContainerList is a mock implementation of Docker's client.ContainerAPIClient.ContainerList() 132 | // 133 | // TODO: properly implement 134 | func (c *ContainerAPIClient) ContainerList(context.Context, 135 | container.ListOptions) ([]container.Summary, error) { 136 | return nil, nil 137 | } 138 | 139 | // ContainerLogs is a mock implementation of Docker's client.ContainerAPIClient.ContainerLogs() 140 | func (c *ContainerAPIClient) ContainerLogs(context.Context, string, 141 | container.LogsOptions) (io.ReadCloser, error) { 142 | if c.Logs == nil { 143 | return nil, Err 144 | } 145 | return c.Logs, nil 146 | } 147 | 148 | // ContainerPause is a mock implementation of Docker's client.ContainerAPIClient.ContainerPause() 149 | // 150 | // TODO: properly implement 151 | func (c *ContainerAPIClient) ContainerPause(context.Context, string) error { return nil } 152 | 153 | // ContainerRemove is a mock implementation of Docker's client.ContainerAPIClient.ContainerRemove() 154 | func (c *ContainerAPIClient) ContainerRemove(context.Context, string, 155 | container.RemoveOptions) error { 156 | return c.RemoveErr 157 | } 158 | 159 | // ContainerRename is a mock implementation of Docker's client.ContainerAPIClient.ContainerRename() 160 | // 161 | // TODO: properly implement 162 | func (c *ContainerAPIClient) ContainerRename(context.Context, string, string) error { 163 | return nil 164 | } 165 | 166 | // ContainerResize is a mock implementation of Docker's client.ContainerAPIClient.ContainerResize() 167 | // 168 | // TODO: properly implement 169 | func (c *ContainerAPIClient) ContainerResize(context.Context, string, container.ResizeOptions) error { 170 | return nil 171 | } 172 | 173 | // ContainerRestart is a mock implementation of Docker's client.ContainerAPIClient.ContainerRestart() 174 | // 175 | // TODO: properly implement 176 | func (c *ContainerAPIClient) ContainerRestart(context.Context, string, container.StopOptions) error { 177 | return nil 178 | } 179 | 180 | // ContainerStatPath is a mock implementation of Docker's client.ContainerAPIClient.ContainerStatPath() 181 | // 182 | // TODO: properly implement 183 | func (c *ContainerAPIClient) ContainerStatPath(context.Context, string, 184 | string) (container.PathStat, error) { 185 | return container.PathStat{}, nil 186 | } 187 | 188 | // ContainerStats is a mock implementation of Docker's client.ContainerAPIClient.ContainerStats() 189 | // 190 | // TODO: properly implement 191 | func (c *ContainerAPIClient) ContainerStats(context.Context, string, 192 | bool) (container.StatsResponseReader, error) { 193 | return container.StatsResponseReader{}, nil 194 | } 195 | 196 | // ContainerStatsOneShot is a mock implementation of Docker's client.ContainerAPIClient.ContainerStatsOneShot() 197 | // 198 | // TODO: properly implement 199 | func (c *ContainerAPIClient) ContainerStatsOneShot(context.Context, string) (container.StatsResponseReader, error) { 200 | return container.StatsResponseReader{}, nil 201 | } 202 | 203 | // ContainerStart is a mock implementation of Docker's client.ContainerAPIClient.ContainerStart() 204 | func (c *ContainerAPIClient) ContainerStart(context.Context, string, 205 | container.StartOptions) error { 206 | return c.StartErr 207 | } 208 | 209 | // ContainerStop is a mock implementation of Docker's client.ContainerAPIClient.ContainerStop() 210 | func (c *ContainerAPIClient) ContainerStop(context.Context, string, container.StopOptions) error { 211 | return c.StopErr 212 | } 213 | 214 | // ContainerTop is a mock implementation of Docker's client.ContainerAPIClient.ContainerTop() 215 | // 216 | // TODO: properly implement 217 | func (c *ContainerAPIClient) ContainerTop(context.Context, string, 218 | []string) (container.TopResponse, error) { 219 | return container.TopResponse{}, nil 220 | } 221 | 222 | // ContainerUnpause is a mock implementation of Docker's client.ContainerAPIClient.ContainerUnpause() 223 | // 224 | // TODO: properly implement 225 | func (c *ContainerAPIClient) ContainerUnpause(context.Context, string) error { 226 | return nil 227 | } 228 | 229 | // ContainerUpdate is a mock implementation of Docker's client.ContainerAPIClient.ContainerUpdate() 230 | // 231 | // TODO: properly implement 232 | func (c *ContainerAPIClient) ContainerUpdate(context.Context, string, 233 | container.UpdateConfig) (container.UpdateResponse, error) { 234 | return container.UpdateResponse{}, nil 235 | } 236 | 237 | // ContainerWait is a mock implementation of Docker's client.ContainerAPIClient.ContainerWait() 238 | // 239 | // TODO: properly implement 240 | func (c *ContainerAPIClient) ContainerWait(context.Context, string, 241 | container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { 242 | return nil, nil 243 | } 244 | 245 | // CopyFromContainer is a mock implementation of Docker's client.ContainerAPIClient.CopyFromContainer() 246 | // 247 | // TODO: properly implement 248 | func (c *ContainerAPIClient) CopyFromContainer(context.Context, string, string) (io.ReadCloser, 249 | container.PathStat, error) { 250 | return nil, container.PathStat{}, nil 251 | } 252 | 253 | // CopyToContainer is a mock implementation of Docker's client.ContainerAPIClient.CopyToContainer() 254 | // 255 | // TODO: properly implement 256 | func (c *ContainerAPIClient) CopyToContainer(context.Context, string, string, io.Reader, 257 | container.CopyToContainerOptions) error { 258 | return nil 259 | } 260 | 261 | // ContainersPrune is a mock implementation of Docker's client.ContainerAPIClient.ContainersPrune() 262 | // 263 | // TODO: properly implement 264 | func (c *ContainerAPIClient) ContainersPrune(context.Context, filters.Args) (container.PruneReport, error) { 265 | return container.PruneReport{}, nil 266 | } 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 2 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 3 | github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= 4 | github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= 5 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 6 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 7 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 8 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 9 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 10 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 11 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 12 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 13 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 14 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 18 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 19 | github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= 20 | github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 21 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 22 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 23 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 24 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 25 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 26 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 27 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 28 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 29 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 30 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 31 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 32 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 33 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 34 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 38 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 39 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 40 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 41 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 42 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 43 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 44 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 45 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 46 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 47 | github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= 48 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 49 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 50 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 51 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 52 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 53 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 54 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 55 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= 56 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= 57 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 58 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 59 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 60 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 61 | github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= 62 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 63 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 64 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 65 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 69 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 70 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 71 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 72 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 73 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 74 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 75 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 76 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 77 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 78 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 79 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 80 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 81 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= 82 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= 83 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 84 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 85 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= 86 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= 87 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= 88 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= 89 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 90 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 91 | go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= 92 | go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= 93 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 94 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 95 | go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= 96 | go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= 97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 98 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 99 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 100 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 101 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 102 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 103 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 107 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 108 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 109 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 120 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 121 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 122 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 124 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 125 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= 126 | golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 129 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 130 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 131 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 132 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 137 | google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= 138 | google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= 139 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= 140 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 141 | google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= 142 | google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 143 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 144 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 145 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 146 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 147 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 148 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 149 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 150 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 151 | gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= 152 | gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= 153 | --------------------------------------------------------------------------------