├── container ├── testdata │ ├── empty.sh │ ├── hello.sh │ ├── Dockerfile │ └── waitForHello.sh ├── Makefile ├── wait │ ├── testdata │ │ ├── http │ │ │ ├── go.mod │ │ │ ├── tls-key.pem │ │ │ ├── Dockerfile │ │ │ ├── tls.pem │ │ │ └── main.go │ │ ├── cert.key │ │ ├── root.pem │ │ └── cert.crt │ ├── errors_windows.go │ ├── errors.go │ ├── exit_test.go │ ├── wait.go │ ├── walk.go │ ├── wait_test.go │ ├── file_test.go │ ├── nop.go │ ├── exit.go │ └── health.go ├── generate.go ├── version.go ├── .mockery.yaml ├── container.stop_test.go ├── lifecycle.stop.go ├── lifecycle.terminate.go ├── exec │ └── types.go ├── container.start.go ├── container_unit_test.go ├── lifecycle.start.go ├── log │ └── logger.go ├── ports_benchmarks_test.go ├── container.terminate_test.go ├── inspect.go ├── container.from_test.go ├── definition_test.go ├── container.network.go ├── container.exec.go ├── testing.go ├── ports.go ├── image_substitutors_test.go ├── container.stop.go └── go.mod ├── image ├── testdata │ ├── build │ │ ├── ignoreme │ │ ├── .dockerignore │ │ ├── say_hi.sh │ │ └── Dockerfile │ ├── retry │ │ └── Dockerfile │ ├── Dockerfile │ ├── Dockerfile.multistage │ ├── dockerignore │ │ └── .dockerignore │ ├── Dockerfile.multistage.singleBuildArgs │ ├── Dockerfile.multistage.singleBuildArgs.defaults │ └── Dockerfile.multistage.multiBuildArgs ├── Makefile ├── version.go ├── dockerignore_test.go ├── save_examples_test.go ├── dockerignore.go ├── options_test.go ├── remove.go ├── build.log.go ├── remove_test.go ├── save.go ├── save_test.go ├── build_benchmarks_test.go ├── mocks_test.go ├── go.mod ├── pull_examples_test.go ├── build_unit_test.go ├── build_examples_test.go ├── dockerfiles_test.go └── pull_test.go ├── client ├── Makefile ├── version.go ├── client.volume.go ├── client.image.go ├── client.network.go ├── errors.go ├── examples_test.go ├── labels_test.go ├── errors_test.go ├── testdata │ └── certificates │ │ ├── cert.pem │ │ ├── ca.pem │ │ └── key.pem ├── labels.go ├── config_test.go ├── client.container.go ├── options_test.go ├── README.md ├── go.mod ├── config.go ├── daemon.go ├── client.container_benchmarks_test.go └── client_benchmark_test.go ├── config ├── Makefile ├── testdata │ ├── invalid-config │ │ └── .docker │ │ │ └── config.json │ ├── .docker │ │ └── config.json │ └── credhelpers-config │ │ └── config.json ├── version.go ├── go.mod ├── auth_examples_test.go ├── load_examples_test.go ├── config_benchmarks_test.go ├── credentials_helpers_test.go ├── auth │ ├── registry_unit_test.go │ └── registry.go ├── auth.go └── README.md ├── context ├── Makefile ├── version.go ├── host_unix.go ├── host_windows.go ├── errors.go ├── go.mod ├── current_test.go ├── context.delete_test.go ├── context.delete.go ├── rootless_test.go ├── rootless.go ├── options.go ├── context.add.go ├── go.sum ├── context.add_test.go └── context_examples_test.go ├── network ├── Makefile ├── version.go ├── network.terminate_test.go ├── types.go ├── network.terminate.go ├── network.go ├── go.mod ├── options.go ├── README.md ├── network.inspect.go └── network.list_test.go ├── volume ├── Makefile ├── version.go ├── types.go ├── volume.terminate_test.go ├── volume.terminate.go ├── testing_test.go ├── volume.go ├── go.mod ├── volume.find.go ├── volume_examples_test.go ├── options.go └── README.md ├── legacyadapters ├── Makefile ├── version.go ├── doc.go └── go.mod ├── .gitignore ├── go.work ├── .github ├── workflows │ ├── sonar-bulk-operations.yml │ ├── sonar-create-project.yml │ ├── sonar-delete-project.yml │ ├── ci-lint-go.yml │ ├── conventions.yml │ ├── release-drafter.yml │ └── release-modules.yml ├── PULL_REQUEST_TEMPLATE.md ├── release-drafter-template.yml └── scripts │ ├── refresh-proxy.sh │ └── check-pre-release.sh ├── NOTICE ├── third_party ├── cpuguy83 │ └── dockercfg │ │ └── LICENSE └── testcontainers │ └── testcontainers-go │ └── LICENSE ├── Makefile └── .golangci.yml /container/testdata/empty.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image/testdata/build/ignoreme: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /config/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /context/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /image/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /image/testdata/build/.dockerignore: -------------------------------------------------------------------------------- 1 | ignoreme 2 | -------------------------------------------------------------------------------- /image/testdata/retry/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | -------------------------------------------------------------------------------- /network/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /volume/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /container/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk 2 | -------------------------------------------------------------------------------- /legacyadapters/Makefile: -------------------------------------------------------------------------------- 1 | include ../commons-test.mk -------------------------------------------------------------------------------- /container/wait/testdata/http/go.mod: -------------------------------------------------------------------------------- 1 | module httptest 2 | 3 | go 1.24 4 | -------------------------------------------------------------------------------- /container/generate.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | //go:generate mockery 4 | -------------------------------------------------------------------------------- /image/testdata/build/say_hi.sh: -------------------------------------------------------------------------------- 1 | echo hi this is from the say_hi.sh file! 2 | -------------------------------------------------------------------------------- /config/testdata/invalid-config/.docker/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": [] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .github/scripts/.build 2 | coverage.out 3 | output.txt 4 | TEST-unit.xml 5 | -------------------------------------------------------------------------------- /container/testdata/hello.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "hello world" > /tmp/hello.txt 3 | echo "done" -------------------------------------------------------------------------------- /image/testdata/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG tag=latest 2 | FROM nginx:${tag} 3 | COPY . . 4 | CMD ["pwd"] 5 | -------------------------------------------------------------------------------- /container/testdata/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | CMD ["echo", "this is from the echo test Dockerfile"] -------------------------------------------------------------------------------- /image/testdata/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | WORKDIR /app 3 | COPY . . 4 | CMD ["sh", "./say_hi.sh"] 5 | -------------------------------------------------------------------------------- /image/testdata/Dockerfile.multistage: -------------------------------------------------------------------------------- 1 | FROM nginx:a as builderA 2 | FROM nginx:b as builderB 3 | FROM nginx:c as builderC 4 | FROM scratch 5 | -------------------------------------------------------------------------------- /image/testdata/dockerignore/.dockerignore: -------------------------------------------------------------------------------- 1 | # .dockerignore 2 | # https://docs.docker.com/build/building/context/#dockerignore-files 3 | vendor 4 | foo 5 | bar -------------------------------------------------------------------------------- /container/testdata/waitForHello.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | file=/scripts/hello.sh 4 | 5 | until [ -s "$file" ] 6 | do 7 | sleep 0.1 8 | done 9 | 10 | sh $file -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.24.0 2 | 3 | use ( 4 | ./client 5 | ./config 6 | ./container 7 | ./context 8 | ./image 9 | ./legacyadapters 10 | ./network 11 | ./volume 12 | ) 13 | -------------------------------------------------------------------------------- /image/testdata/Dockerfile.multistage.singleBuildArgs: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=scratch 2 | FROM nginx:a as builderA 3 | FROM nginx:b as builderB 4 | FROM nginx:c as builderC 5 | FROM ${BASE_IMAGE} 6 | -------------------------------------------------------------------------------- /image/testdata/Dockerfile.multistage.singleBuildArgs.defaults: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=scratch 2 | FROM nginx:a as builderA 3 | FROM nginx:b as builderB 4 | FROM nginx:c as builderC 5 | FROM ${BASE_IMAGE:-nginx:d} 6 | -------------------------------------------------------------------------------- /container/wait/errors_windows.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "golang.org/x/sys/windows" 5 | ) 6 | 7 | func isConnRefusedErr(err error) bool { 8 | return err == windows.WSAECONNREFUSED 9 | } 10 | -------------------------------------------------------------------------------- /client/version.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | const ( 4 | version = "0.1.0-alpha011" 5 | ) 6 | 7 | // Version returns the version of the client package. 8 | func Version() string { 9 | return version 10 | } 11 | -------------------------------------------------------------------------------- /config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | version = "0.1.0-alpha011" 5 | ) 6 | 7 | // Version returns the version of the config package. 8 | func Version() string { 9 | return version 10 | } 11 | -------------------------------------------------------------------------------- /context/version.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | const ( 4 | version = "0.1.0-alpha011" 5 | ) 6 | 7 | // Version returns the version of the context package. 8 | func Version() string { 9 | return version 10 | } 11 | -------------------------------------------------------------------------------- /legacyadapters/version.go: -------------------------------------------------------------------------------- 1 | package legacyadapters 2 | 3 | const ( 4 | version = "0.1.0-alpha003" 5 | ) 6 | 7 | // Version returns the version of the legacyadapters package. 8 | func Version() string { 9 | return version 10 | } 11 | -------------------------------------------------------------------------------- /config/testdata/.docker/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | "https://index.docker.io/v1/": {}, 4 | "https://example.com": {}, 5 | "https://my.private.registry": {} 6 | }, 7 | "credsStore": "desktop" 8 | } 9 | -------------------------------------------------------------------------------- /container/wait/errors.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package wait 5 | 6 | import ( 7 | "errors" 8 | "syscall" 9 | ) 10 | 11 | func isConnRefusedErr(err error) bool { 12 | return errors.Is(err, syscall.ECONNREFUSED) 13 | } 14 | -------------------------------------------------------------------------------- /container/wait/testdata/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIM8HuDwcZyVqBBy2C6db6zNb/dAJ69bq5ejAEz7qGOIQoAoGCCqGSM49 3 | AwEHoUQDQgAEBL2ioRmfTc70WT0vyx+amSQOGbMeoMRAfF2qaPzpzOqpKTk0aLOG 4 | 0735iy9Fz16PX4vqnLMiM/ZupugAhB//yA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /container/wait/testdata/http/tls-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIM8HuDwcZyVqBBy2C6db6zNb/dAJ69bq5ejAEz7qGOIQoAoGCCqGSM49 3 | AwEHoUQDQgAEBL2ioRmfTc70WT0vyx+amSQOGbMeoMRAfF2qaPzpzOqpKTk0aLOG 4 | 0735iy9Fz16PX4vqnLMiM/ZupugAhB//yA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /image/testdata/Dockerfile.multistage.multiBuildArgs: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=scratch 2 | ARG NGINX_IMAGE=nginx:latest 3 | ARG REGISTRY_HOST=localhost 4 | ARG REGISTRY_PORT=5000 5 | FROM ${NGINX_IMAGE} as builderA 6 | FROM ${REGISTRY_HOST}:${REGISTRY_PORT}/${NGINX_IMAGE} as builderB 7 | FROM ${BASE_IMAGE} 8 | -------------------------------------------------------------------------------- /image/version.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import "github.com/docker/go-sdk/client" 4 | 5 | const ( 6 | version = "0.1.0-alpha012" 7 | moduleLabel = client.LabelBase + ".image" 8 | ) 9 | 10 | // Version returns the version of the image package. 11 | func Version() string { 12 | return version 13 | } 14 | -------------------------------------------------------------------------------- /network/version.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import "github.com/docker/go-sdk/client" 4 | 5 | const ( 6 | version = "0.1.0-alpha011" 7 | moduleLabel = client.LabelBase + ".network" 8 | ) 9 | 10 | // Version returns the version of the network package. 11 | func Version() string { 12 | return version 13 | } 14 | -------------------------------------------------------------------------------- /volume/version.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import "github.com/docker/go-sdk/client" 4 | 5 | const ( 6 | version = "0.1.0-alpha003" 7 | moduleLabel = client.LabelBase + ".volume" 8 | ) 9 | 10 | // Version returns the version of the volume package. 11 | func Version() string { 12 | return version 13 | } 14 | -------------------------------------------------------------------------------- /container/version.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import "github.com/docker/go-sdk/client" 4 | 5 | const ( 6 | version = "0.1.0-alpha012" 7 | moduleLabel = client.LabelBase + ".container" 8 | ) 9 | 10 | // Version returns the version of the container package. 11 | func Version() string { 12 | return version 13 | } 14 | -------------------------------------------------------------------------------- /config/testdata/credhelpers-config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auths": { 3 | "userpass.io": { 4 | "username": "user", 5 | "password": "pass" 6 | }, 7 | "auth.io": { 8 | "auth": "YXV0aDphdXRoc2VjcmV0" 9 | } 10 | }, 11 | "credHelpers": { 12 | "helper.io": "helper", 13 | "other.io": "other", 14 | "error.io": "error" 15 | }, 16 | "credsStore": "desktop" 17 | } 18 | -------------------------------------------------------------------------------- /context/host_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package context 5 | 6 | func init() { 7 | // DefaultSchema is the default schema to use for the Docker host on Linux 8 | DefaultSchema = "unix://" 9 | 10 | // DefaultDockerHost is the default host to connect to the Docker socket on Linux 11 | DefaultDockerHost = DefaultSchema + "/var/run/docker.sock" 12 | } 13 | -------------------------------------------------------------------------------- /context/host_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package context 5 | 6 | func init() { 7 | // DefaultSchema is the default schema to use for the Docker host on Windows 8 | DefaultSchema = "npipe://" 9 | 10 | // DefaultDockerHost is the default host to connect to the Docker socket on Windows 11 | DefaultDockerHost = DefaultSchema + "//./pipe/docker_engine" 12 | } 13 | -------------------------------------------------------------------------------- /context/errors.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrDockerHostNotSet is returned when the Docker host is not set in the Docker context. 7 | ErrDockerHostNotSet = errors.New("docker host not set in Docker context") 8 | 9 | // ErrDockerContextNotFound is returned when the Docker context is not found. 10 | ErrDockerContextNotFound = errors.New("docker context not found") 11 | ) 12 | -------------------------------------------------------------------------------- /container/.mockery.yaml: -------------------------------------------------------------------------------- 1 | log-level: "info" 2 | disable-version-string: True 3 | resolve-type-alias: False 4 | issue-845-fix: True 5 | with-expecter: True 6 | mockname: "mock{{.InterfaceName}}" 7 | filename: "{{ .InterfaceName | lower }}_mock_test.go" 8 | outpkg: "{{.PackageName}}_test" 9 | dir: "{{.InterfaceDir}}" 10 | packages: 11 | github.com/docker/go-sdk/container/wait: 12 | interfaces: 13 | StrategyTarget: 14 | -------------------------------------------------------------------------------- /container/wait/testdata/http/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine3.22@sha256:92b6d90efb23fa6ee3eef6d45f5d06527e56b0d3100e98eb5a1d9b3b8acd9bca as builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN mkdir -p dist 5 | RUN go build -o ./dist/server main.go 6 | 7 | FROM alpine 8 | WORKDIR /app 9 | COPY --from=builder /app/tls.pem /app/tls-key.pem ./ 10 | COPY --from=builder /app/dist/server . 11 | EXPOSE 6443 12 | CMD ["/app/server"] 13 | -------------------------------------------------------------------------------- /client/client.volume.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/moby/moby/client" 7 | ) 8 | 9 | // VolumeCreate creates a new volume. 10 | func (c *sdkClient) VolumeCreate(ctx context.Context, options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { 11 | // Add the labels that identify this as a volume created by the SDK. 12 | AddSDKLabels(options.Labels) 13 | 14 | return c.APIClient.VolumeCreate(ctx, options) 15 | } 16 | -------------------------------------------------------------------------------- /client/client.image.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/moby/moby/client" 8 | ) 9 | 10 | // ImageBuild builds an image from a build context and options. 11 | func (c *sdkClient) ImageBuild(ctx context.Context, context io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) { 12 | // Add client labels 13 | AddSDKLabels(options.Labels) 14 | 15 | return c.APIClient.ImageBuild(ctx, context, options) 16 | } 17 | -------------------------------------------------------------------------------- /client/client.network.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/moby/moby/client" 7 | ) 8 | 9 | // NetworkCreate creates a new network 10 | func (c *sdkClient) NetworkCreate(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) { 11 | // Add the labels that identify this as a network created by the SDK. 12 | AddSDKLabels(options.Labels) 13 | 14 | return c.APIClient.NetworkCreate(ctx, name, options) 15 | } 16 | -------------------------------------------------------------------------------- /container/wait/testdata/root.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBVTCB/aADAgECAghLWuRKnTb4BjAKBggqhkjOPQQDAjAAMB4XDTIwMDgxOTEz 3 | MzUwOFoXDTMwMDgxNzEzNDAwOFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA 4 | BP39G8oZK7JvdcJzSuEzoqe3KsWS7/4C7UKhdoGHkEuHED+I456v3O8x0gUTjqIv 5 | I9FmW3cq/eMoraPzzk3u7vajYTBfMA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAU 6 | BggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU 7 | FdfV6PSYUlHs+lSQNouRwSfR2ZgwCgYIKoZIzj0EAwIDRwAwRAIgDAFtSEaFGuvP 8 | wJZhQv7zjIhCGzYzsZ8KSKUJ3YvdL/4CIBbgDFzEeQWFWUMFPeMaVVrmBmsflPIg 9 | cnC4yG76skGg 10 | -----END CERTIFICATE----- 11 | -------------------------------------------------------------------------------- /volume/types.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | dockervolume "github.com/moby/moby/api/types/volume" 5 | 6 | "github.com/docker/go-sdk/client" 7 | ) 8 | 9 | // Volume represents a Docker volume. 10 | type Volume struct { 11 | *dockervolume.Volume 12 | dockerClient client.SDKClient 13 | } 14 | 15 | // ID is an alias for the Name field, as it coincides with the Name of the volume. 16 | func (v *Volume) ID() string { 17 | return v.Name 18 | } 19 | 20 | // Client returns the client used to create the volume. 21 | func (v *Volume) Client() client.SDKClient { 22 | return v.dockerClient 23 | } 24 | -------------------------------------------------------------------------------- /client/errors.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/containerd/errdefs" 5 | ) 6 | 7 | var permanentClientErrors = []func(error) bool{ 8 | errdefs.IsNotFound, 9 | errdefs.IsInvalidArgument, 10 | errdefs.IsUnauthorized, 11 | errdefs.IsPermissionDenied, 12 | errdefs.IsNotImplemented, 13 | errdefs.IsInternal, 14 | } 15 | 16 | // IsPermanentClientError returns true if the error is a permanent client error. 17 | func IsPermanentClientError(err error) bool { 18 | for _, isErrFn := range permanentClientErrors { 19 | if isErrFn(err) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | -------------------------------------------------------------------------------- /client/examples_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | dockerclient "github.com/moby/moby/client" 9 | 10 | "github.com/docker/go-sdk/client" 11 | ) 12 | 13 | func ExampleNew() { 14 | cli, err := client.New(context.Background()) 15 | if err != nil { 16 | log.Printf("error creating client: %s", err) 17 | return 18 | } 19 | 20 | info, err := cli.Info(context.Background(), dockerclient.InfoOptions{}) 21 | if err != nil { 22 | log.Printf("error getting info: %s", err) 23 | return 24 | } 25 | 26 | fmt.Println(info.Info.OperatingSystem != "") 27 | 28 | // Output: 29 | // true 30 | } 31 | -------------------------------------------------------------------------------- /legacyadapters/doc.go: -------------------------------------------------------------------------------- 1 | // Package legacyadapters provides conversion utilities to bridge between 2 | // the modern Docker Go SDK types and legacy Docker CLI/Docker Engine API types. 3 | // 4 | // Deprecated: This entire module is deprecated and temporary. It will be removed 5 | // in a future release when all Docker products have migrated to use the go-sdk 6 | // natively. We strongly recommend avoiding this module in new projects and using 7 | // the native go-sdk types directly instead. 8 | // 9 | // This module exists solely to provide a migration path for existing Docker 10 | // products during the transition period. 11 | package legacyadapters 12 | -------------------------------------------------------------------------------- /config/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/config 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/distribution/reference v0.6.0 7 | github.com/moby/moby/api v1.52.0 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/kr/pretty v0.3.1 // indirect 14 | github.com/opencontainers/go-digest v1.0.0 // indirect 15 | github.com/opencontainers/image-spec v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/rogpeppe/go-internal v1.13.1 // indirect 18 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /context/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/context 2 | 3 | go 1.24.0 4 | 5 | replace github.com/docker/go-sdk/config => ../config 6 | 7 | require ( 8 | github.com/docker/go-sdk/config v0.1.0-alpha011 9 | github.com/opencontainers/go-digest v1.0.0 10 | github.com/stretchr/testify v1.10.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/distribution/reference v0.6.0 // indirect 16 | github.com/kr/text v0.2.0 // indirect 17 | github.com/moby/moby/api v1.52.0 // indirect 18 | github.com/opencontainers/image-spec v1.1.1 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /container/container.stop_test.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestStopOptions(t *testing.T) { 12 | t.Run("with-stop-timeout", func(t *testing.T) { 13 | opts := NewStopOptions(context.Background(), StopTimeout(10*time.Second)) 14 | require.Equal(t, 10*time.Second, opts.StopTimeout()) 15 | }) 16 | 17 | t.Run("with-stop-timeout-and-context", func(t *testing.T) { 18 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 19 | defer cancel() 20 | opts := NewStopOptions(ctx, StopTimeout(10*time.Second)) 21 | require.Equal(t, ctx, opts.Context()) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /config/auth_examples_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/docker/go-sdk/config" 7 | "github.com/docker/go-sdk/config/auth" 8 | ) 9 | 10 | func ExampleAuthConfigs() { 11 | authConfigs, err := config.AuthConfigs("nginx:latest") 12 | fmt.Println(err) 13 | fmt.Println(len(authConfigs)) 14 | fmt.Println(authConfigs[auth.DockerRegistry].ServerAddress) 15 | 16 | // Output: 17 | // 18 | // 1 19 | // docker.io 20 | } 21 | 22 | func ExampleAuthConfigForHostname() { 23 | authConfig, err := config.AuthConfigForHostname(auth.IndexDockerIO) 24 | fmt.Println(err) 25 | fmt.Println(authConfig.Username != "") 26 | 27 | // Output: 28 | // 29 | // true 30 | } 31 | -------------------------------------------------------------------------------- /container/lifecycle.stop.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import "context" 4 | 5 | // stoppingHook is a hook that will be called before a container is stopped. 6 | func (c *Container) stoppingHook(ctx context.Context) error { 7 | return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { 8 | return applyContainerHooks(ctx, lifecycleHooks.PreStops, c) 9 | }) 10 | } 11 | 12 | // stoppedHook is a hook that will be called after a container is stopped. 13 | func (c *Container) stoppedHook(ctx context.Context) error { 14 | return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { 15 | return applyContainerHooks(ctx, lifecycleHooks.PostStops, c) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /container/lifecycle.terminate.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import "context" 4 | 5 | // terminatingHook is a hook that will be called before a container is terminated. 6 | func (c *Container) terminatingHook(ctx context.Context) error { 7 | return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { 8 | return applyContainerHooks(ctx, lifecycleHooks.PreTerminates, c) 9 | }) 10 | } 11 | 12 | // stoppedHook is a hook that will be called after a container is stopped. 13 | func (c *Container) terminatedHook(ctx context.Context) error { 14 | return c.applyLifecycleHooks(ctx, false, func(lifecycleHooks LifecycleHooks) error { 15 | return applyContainerHooks(ctx, lifecycleHooks.PostTerminates, c) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /volume/volume.terminate_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/docker/go-sdk/volume" 10 | ) 11 | 12 | func TestVolumeTerminate(t *testing.T) { 13 | t.Run("success", func(t *testing.T) { 14 | v, err := volume.New(context.Background()) 15 | require.NoError(t, err) 16 | require.NoError(t, v.Terminate(context.Background())) 17 | 18 | // Safe to cleanup (ErrNotFound) 19 | volume.Cleanup(t, v) 20 | }) 21 | 22 | t.Run("with-force", func(t *testing.T) { 23 | v, err := volume.New(context.Background()) 24 | require.NoError(t, err) 25 | require.NoError(t, v.Terminate(context.Background(), volume.WithForce())) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /container/wait/testdata/cert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBxTCCAWugAwIBAgIUWBLNpiF1o4r+5ZXwawzPOfBM1F8wCgYIKoZIzj0EAwIw 3 | ADAeFw0yMDA4MTkxMzM4MDBaFw0zMDA4MTcxMzM4MDBaMBkxFzAVBgNVBAMTDnRl 4 | c3Rjb250YWluZXJzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBL2ioRmfTc70 5 | WT0vyx+amSQOGbMeoMRAfF2qaPzpzOqpKTk0aLOG0735iy9Fz16PX4vqnLMiM/Zu 6 | pugAhB//yKOBqTCBpjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH 7 | AwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUTMdz5PIZ+Gix4jYUzRIHfByrW+Yw 8 | HwYDVR0jBBgwFoAUFdfV6PSYUlHs+lSQNouRwSfR2ZgwMQYDVR0RBCowKIIVdGVz 9 | dGNvbnRhaW5lci5nby50ZXN0gglsb2NhbGhvc3SHBH8AAAEwCgYIKoZIzj0EAwID 10 | SAAwRQIhAJznPNumi2Plf0GsP9DpC+8WukT/jUhnhcDWCfZ6Ini2AiBLhnhFebZX 11 | XWfSsdSNxIo20OWvy6z3wqdybZtRUfdU+g== 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /container/wait/testdata/http/tls.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBxTCCAWugAwIBAgIUWBLNpiF1o4r+5ZXwawzPOfBM1F8wCgYIKoZIzj0EAwIw 3 | ADAeFw0yMDA4MTkxMzM4MDBaFw0zMDA4MTcxMzM4MDBaMBkxFzAVBgNVBAMTDnRl 4 | c3Rjb250YWluZXJzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBL2ioRmfTc70 5 | WT0vyx+amSQOGbMeoMRAfF2qaPzpzOqpKTk0aLOG0735iy9Fz16PX4vqnLMiM/Zu 6 | pugAhB//yKOBqTCBpjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH 7 | AwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUTMdz5PIZ+Gix4jYUzRIHfByrW+Yw 8 | HwYDVR0jBBgwFoAUFdfV6PSYUlHs+lSQNouRwSfR2ZgwMQYDVR0RBCowKIIVdGVz 9 | dGNvbnRhaW5lci5nby50ZXN0gglsb2NhbGhvc3SHBH8AAAEwCgYIKoZIzj0EAwID 10 | SAAwRQIhAJznPNumi2Plf0GsP9DpC+8WukT/jUhnhcDWCfZ6Ini2AiBLhnhFebZX 11 | XWfSsdSNxIo20OWvy6z3wqdybZtRUfdU+g== 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /network/network.terminate_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/docker/go-sdk/network" 10 | ) 11 | 12 | func TestTerminate(t *testing.T) { 13 | dockerClient, _ := testClientWithLogger(t) 14 | defer dockerClient.Close() 15 | 16 | t.Run("network-does-not-exist", func(t *testing.T) { 17 | n := &network.Network{} 18 | require.Error(t, n.Terminate(context.Background())) 19 | }) 20 | 21 | t.Run("network-exist", func(t *testing.T) { 22 | nw, err := network.New(context.Background(), 23 | network.WithClient(dockerClient), 24 | ) 25 | require.NoError(t, err) 26 | require.NoError(t, nw.Terminate(context.Background())) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /client/labels_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/docker/go-sdk/client" 9 | ) 10 | 11 | func TestAddSDKLabels(t *testing.T) { 12 | labels := map[string]string{} 13 | 14 | client.AddSDKLabels(labels) 15 | require.Contains(t, labels, client.LabelBase) 16 | require.Contains(t, labels, client.LabelLang) 17 | require.Contains(t, labels, client.LabelVersion) 18 | 19 | t.Run("idempotent", func(t *testing.T) { 20 | sdkLabels := client.SDKLabels() 21 | sdkLabels["foo"] = "bar" 22 | 23 | labels := make(map[string]string) 24 | client.AddSDKLabels(labels) 25 | require.NotEqual(t, sdkLabels, labels) 26 | require.NotContains(t, labels, "foo") 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/sonar-bulk-operations.yml: -------------------------------------------------------------------------------- 1 | name: Sonar Bulk Operations 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | operation: 7 | description: 'Operation to perform' 8 | required: true 9 | type: choice 10 | options: 11 | - createAll 12 | - deleteAll 13 | 14 | jobs: 15 | bulk-operation: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Execute Bulk Operation 21 | env: 22 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 23 | run: | 24 | chmod +x .github/scripts/sonar-manager.sh 25 | .github/scripts/sonar-manager.sh -a "${{ inputs.operation }}" 26 | -------------------------------------------------------------------------------- /.github/workflows/sonar-create-project.yml: -------------------------------------------------------------------------------- 1 | name: Create Sonar Project 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | project_name: 7 | description: 'Name of the project (without type prefix, e.g. client, container, image, network, etc.)' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | create-project: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Create Sonar Project for module 18 | env: 19 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 20 | run: | 21 | chmod +x .github/scripts/sonar-manager.sh 22 | .github/scripts/sonar-manager.sh -a "create" -p "${{ inputs.project_name }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/sonar-delete-project.yml: -------------------------------------------------------------------------------- 1 | name: Delete Sonar Project 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | project_name: 7 | description: 'Name of the project (without type prefix, e.g. client, container, image, network, etc.)' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | delete-project: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Delete Sonar Project for module 18 | env: 19 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 20 | run: | 21 | chmod +x .github/scripts/sonar-manager.sh 22 | .github/scripts/sonar-manager.sh -a "delete" -p "${{ inputs.project_name }}" 23 | -------------------------------------------------------------------------------- /volume/volume.terminate.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/moby/moby/client" 8 | ) 9 | 10 | // TerminableVolume is a volume that can be terminated. 11 | type TerminableVolume interface { 12 | Terminate(ctx context.Context, opts ...TerminateOption) error 13 | } 14 | 15 | // Terminate terminates the volume. 16 | func (v *Volume) Terminate(ctx context.Context, opts ...TerminateOption) error { 17 | terminateOptions := &terminateOptions{} 18 | for _, opt := range opts { 19 | if err := opt(terminateOptions); err != nil { 20 | return fmt.Errorf("apply option: %w", err) 21 | } 22 | } 23 | 24 | _, err := v.dockerClient.VolumeRemove(ctx, v.Name, client.VolumeRemoveOptions{Force: terminateOptions.force}) 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /config/load_examples_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/docker/go-sdk/config" 9 | ) 10 | 11 | func ExampleDir() { 12 | dir, err := config.Dir() 13 | fmt.Println(err) 14 | fmt.Println(strings.HasSuffix(dir, ".docker")) 15 | 16 | // Output: 17 | // 18 | // true 19 | } 20 | 21 | func ExampleFilepath() { 22 | filepath, err := config.Filepath() 23 | if err != nil { 24 | log.Printf("error getting config filepath: %s", err) 25 | return 26 | } 27 | 28 | fmt.Println(strings.HasSuffix(filepath, "config.json")) 29 | 30 | // Output: 31 | // true 32 | } 33 | 34 | func ExampleLoad() { 35 | cfg, err := config.Load() 36 | fmt.Println(err) 37 | fmt.Println(len(cfg.AuthConfigs) > 0) 38 | 39 | // Output: 40 | // 41 | // true 42 | } 43 | -------------------------------------------------------------------------------- /container/exec/types.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | // ExecOptions is a struct that provides a default implementation for the Options method 4 | // of the Executable interface. 5 | type ExecOptions struct { 6 | opts []ProcessOption 7 | } 8 | 9 | func (ce ExecOptions) Options() []ProcessOption { 10 | return ce.opts 11 | } 12 | 13 | // RawCommand is a type that implements Executable and represents a command to be sent to a container 14 | type RawCommand struct { 15 | ExecOptions 16 | cmds []string 17 | } 18 | 19 | func NewRawCommand(cmds []string, opts ...ProcessOption) RawCommand { 20 | return RawCommand{ 21 | cmds: cmds, 22 | ExecOptions: ExecOptions{ 23 | opts: opts, 24 | }, 25 | } 26 | } 27 | 28 | // AsCommand returns the command as a slice of strings 29 | func (r RawCommand) AsCommand() []string { 30 | return r.cmds 31 | } 32 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This NOTICE file is provided in compliance with section 4(d) of the Apache License, Version 2.0. 2 | 3 | Docker SDK for Go 4 | Copyright (c) 2025-present Docker, Inc. 5 | 6 | This product includes software developed at Docker, Inc. (https://www.docker.com). 7 | 8 | Based on the following third party software: 9 | 10 | ## cpuguy/dockercfg (MIT License) 11 | Source: https://github.com/cpuguy83/dockercfg 12 | Author: cpuguy83 and contributors 13 | License: MIT (see third_party/cpuguy83/dockercfg/LICENSE) 14 | 15 | This code has been modified. 16 | 17 | ## testcontainers/testcontainers-go (MIT License) 18 | Source: https://github.com/testcontainers/testcontainers-go 19 | Author: Gianluca Arbezzano and contributors 20 | License: MIT (see third_party/testcontainers/testcontainers-go/LICENSE) 21 | 22 | This code has been modified. 23 | -------------------------------------------------------------------------------- /context/current_test.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestParseURL(t *testing.T) { 10 | t.Run("success", func(t *testing.T) { 11 | path, err := parseURL(DefaultDockerHost) 12 | require.NoError(t, err) 13 | require.Equal(t, DefaultDockerHost, path) 14 | }) 15 | 16 | t.Run("success/tcp", func(t *testing.T) { 17 | path, err := parseURL("tcp://localhost:2375") 18 | require.NoError(t, err) 19 | require.Equal(t, "tcp://localhost:2375", path) 20 | }) 21 | 22 | t.Run("error/invalid-schema", func(t *testing.T) { 23 | _, err := parseURL("http://localhost:2375") 24 | require.Error(t, err) 25 | }) 26 | 27 | t.Run("error/invalid-url", func(t *testing.T) { 28 | _, err := parseURL("~wrong~://**~invalid url~**") 29 | require.Error(t, err) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /container/container.start.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/moby/moby/client" 8 | ) 9 | 10 | // Start will start an already created container 11 | func (c *Container) Start(ctx context.Context) error { 12 | err := c.startingHook(ctx) 13 | if err != nil { 14 | return fmt.Errorf("starting hook: %w", err) 15 | } 16 | 17 | if _, err := c.dockerClient.ContainerStart(ctx, c.ID(), client.ContainerStartOptions{}); err != nil { 18 | return fmt.Errorf("container start: %w", err) 19 | } 20 | defer c.dockerClient.Close() 21 | 22 | err = c.startedHook(ctx) 23 | if err != nil { 24 | return fmt.Errorf("started hook: %w", err) 25 | } 26 | 27 | c.isRunning = true 28 | 29 | err = c.readiedHook(ctx) 30 | if err != nil { 31 | return fmt.Errorf("readied hook: %w", err) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /image/dockerignore_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/docker/go-sdk/image" 10 | ) 11 | 12 | func TestParseDockerIgnore(t *testing.T) { 13 | parse := func(t *testing.T, filePath string, expectedExists bool, expectedErr error, expectedExcluded []string) { 14 | t.Helper() 15 | 16 | exists, excluded, err := image.ParseDockerIgnore(filePath) 17 | require.Equal(t, expectedExists, exists) 18 | require.ErrorIs(t, expectedErr, err) 19 | require.Equal(t, expectedExcluded, excluded) 20 | } 21 | 22 | t.Run("dockerignore", func(t *testing.T) { 23 | parse(t, path.Join("testdata", "dockerignore"), true, nil, []string{"vendor", "foo", "bar"}) 24 | }) 25 | 26 | t.Run("no-dockerignore", func(t *testing.T) { 27 | parse(t, path.Join("testdata", "retry"), false, nil, nil) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /config/config_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moby/moby/api/types/registry" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func BenchmarkAuthConfigCaching(b *testing.B) { 11 | cfg := Config{ 12 | AuthConfigs: map[string]registry.AuthConfig{ 13 | "test.io": {Username: "user", Password: "pass"}, 14 | }, 15 | } 16 | 17 | b.Run("first-access", func(b *testing.B) { 18 | for i := 0; i < b.N; i++ { 19 | cfg.clearAuthCache() 20 | _, err := cfg.AuthConfigForHostname("test.io") 21 | require.NoError(b, err) 22 | } 23 | }) 24 | 25 | b.Run("cached-access", func(b *testing.B) { 26 | // Prime the cache 27 | _, _ = cfg.AuthConfigForHostname("test.io") 28 | 29 | b.ResetTimer() 30 | for i := 0; i < b.N; i++ { 31 | _, err := cfg.AuthConfigForHostname("test.io") 32 | require.NoError(b, err) 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /container/container_unit_test.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/moby/moby/api/types/container" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFromResponse(t *testing.T) { 12 | response := container.Summary{ 13 | ID: "1234567890abcdefgh", 14 | Image: "nginx:latest", 15 | State: "running", 16 | Ports: []container.PortSummary{ 17 | {PublicPort: 80, Type: "tcp"}, 18 | {PublicPort: 8080, Type: "udp"}, 19 | }, 20 | } 21 | 22 | ctr, err := FromResponse(context.Background(), nil, response) 23 | require.NoError(t, err) 24 | require.Equal(t, "1234567890abcdefgh", ctr.ID()) 25 | require.Equal(t, "1234567890ab", ctr.ShortID()) 26 | require.Equal(t, "nginx:latest", ctr.Image()) 27 | require.Equal(t, []string{"80/tcp", "8080/udp"}, ctr.exposedPorts) 28 | require.NotNil(t, ctr.dockerClient) 29 | require.NotNil(t, ctr.logger) 30 | } 31 | -------------------------------------------------------------------------------- /network/types.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | dockerclient "github.com/moby/moby/client" 5 | 6 | "github.com/docker/go-sdk/client" 7 | ) 8 | 9 | // Network represents a Docker network. 10 | type Network struct { 11 | response dockerclient.NetworkCreateResult 12 | inspect dockerclient.NetworkInspectResult 13 | dockerClient client.SDKClient 14 | opts *options 15 | name string 16 | } 17 | 18 | // ID returns the ID of the network. 19 | func (n *Network) ID() string { 20 | return n.response.ID 21 | } 22 | 23 | // Driver returns the driver of the network. 24 | func (n *Network) Driver() string { 25 | return n.opts.driver 26 | } 27 | 28 | // Name returns the name of the network. 29 | func (n *Network) Name() string { 30 | return n.name 31 | } 32 | 33 | // Client returns the client used to create the network. 34 | func (n *Network) Client() client.SDKClient { 35 | return n.dockerClient 36 | } 37 | -------------------------------------------------------------------------------- /config/credentials_helpers_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | // mockExecCommand is a helper function to mock exec.LookPath and exec.Command for testing. 11 | func mockExecCommand(t *testing.T, env ...string) { 12 | t.Helper() 13 | 14 | execLookPath = func(file string) (string, error) { 15 | switch file { 16 | case "docker-credential-helper": 17 | return os.Args[0], nil 18 | case "docker-credential-error": 19 | return "", errors.New("lookup error") 20 | } 21 | 22 | return "", exec.ErrNotFound 23 | } 24 | 25 | execCommand = func(name string, arg ...string) *exec.Cmd { 26 | cmd := exec.Command(name, arg...) 27 | cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1") 28 | cmd.Env = append(cmd.Env, env...) 29 | return cmd 30 | } 31 | 32 | t.Cleanup(func() { 33 | execLookPath = exec.LookPath 34 | execCommand = exec.Command 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /legacyadapters/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/legacyadapters 2 | 3 | go 1.24.0 4 | 5 | replace github.com/docker/go-sdk/config => ../config 6 | 7 | require ( 8 | github.com/docker/cli v28.3.2+incompatible 9 | github.com/docker/go-sdk/config v0.1.0-alpha011 10 | github.com/moby/moby/api v1.52.0 11 | github.com/stretchr/testify v1.10.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/distribution/reference v0.6.0 // indirect 17 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/opencontainers/go-digest v1.0.0 // indirect 20 | github.com/opencontainers/image-spec v1.1.1 // indirect 21 | github.com/pkg/errors v0.9.1 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/sirupsen/logrus v1.9.3 // indirect 24 | golang.org/x/sys v0.33.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /client/errors_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/containerd/errdefs" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/client" 11 | ) 12 | 13 | func TestIsPermanentClientError(t *testing.T) { 14 | t.Run("permanent-client-errors", func(t *testing.T) { 15 | require.True(t, client.IsPermanentClientError(errdefs.ErrNotFound)) 16 | require.True(t, client.IsPermanentClientError(errdefs.ErrInvalidArgument)) 17 | require.True(t, client.IsPermanentClientError(errdefs.ErrUnauthenticated)) 18 | require.True(t, client.IsPermanentClientError(errdefs.ErrPermissionDenied)) 19 | require.True(t, client.IsPermanentClientError(errdefs.ErrNotImplemented)) 20 | require.True(t, client.IsPermanentClientError(errdefs.ErrInternal)) 21 | }) 22 | 23 | t.Run("non-permanent-client-errors", func(t *testing.T) { 24 | require.False(t, client.IsPermanentClientError(errors.New("test"))) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /image/save_examples_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/docker/go-sdk/image" 11 | ) 12 | 13 | func ExampleSave() { 14 | img := "redis:alpine" 15 | 16 | err := image.Pull(context.Background(), img) 17 | if err != nil { 18 | log.Println("error pulling image", err) 19 | return 20 | } 21 | 22 | tmpDir := os.TempDir() 23 | 24 | output := filepath.Join(tmpDir, "images.tar") 25 | err = image.Save(context.Background(), output, img) 26 | if err != nil { 27 | log.Println("error saving image", err) 28 | return 29 | } 30 | defer func() { 31 | err := os.Remove(output) 32 | if err != nil { 33 | log.Println("error removing image", err) 34 | } 35 | }() 36 | 37 | info, err := os.Stat(output) 38 | if err != nil { 39 | log.Println("error getting image info", err) 40 | return 41 | } 42 | 43 | fmt.Println(info.Size() > 0) 44 | 45 | // Output: 46 | // true 47 | } 48 | -------------------------------------------------------------------------------- /config/auth/registry_unit_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestResolveRegistryHost(t *testing.T) { 10 | require.Equal(t, IndexDockerIO, ResolveRegistryHost("index.docker.io")) 11 | require.Equal(t, IndexDockerIO, ResolveRegistryHost("index.docker.io/v1")) 12 | require.Equal(t, IndexDockerIO, ResolveRegistryHost("index.docker.io/v1/")) 13 | require.Equal(t, IndexDockerIO, ResolveRegistryHost("docker.io")) 14 | require.Equal(t, IndexDockerIO, ResolveRegistryHost("registry-1.docker.io")) 15 | require.Equal(t, "foobar.com", ResolveRegistryHost("foobar.com")) 16 | require.Equal(t, "http://foobar.com", ResolveRegistryHost("http://foobar.com")) 17 | require.Equal(t, "https://foobar.com", ResolveRegistryHost("https://foobar.com")) 18 | require.Equal(t, "http://foobar.com:8080", ResolveRegistryHost("http://foobar.com:8080")) 19 | require.Equal(t, "https://foobar.com:8080", ResolveRegistryHost("https://foobar.com:8080")) 20 | } 21 | -------------------------------------------------------------------------------- /image/dockerignore.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/moby/patternmatcher/ignorefile" 9 | ) 10 | 11 | // ParseDockerIgnore returns if the file exists, the excluded files and an error if any 12 | func ParseDockerIgnore(targetDir string) (bool, []string, error) { 13 | // based on https://github.com/docker/cli/blob/master/cli/command/image/build/dockerignore.go#L14 14 | fileLocation := filepath.Join(targetDir, ".dockerignore") 15 | var excluded []string 16 | exists := false 17 | f, openErr := os.Open(fileLocation) 18 | if openErr != nil { 19 | if !os.IsNotExist(openErr) { 20 | return false, nil, fmt.Errorf("open .dockerignore: %w", openErr) 21 | } 22 | return false, nil, nil 23 | } 24 | defer f.Close() 25 | 26 | exists = true 27 | var err error 28 | excluded, err = ignorefile.ReadAll(f) 29 | if err != nil { 30 | return true, excluded, fmt.Errorf("read .dockerignore: %w", err) 31 | } 32 | 33 | return exists, excluded, nil 34 | } 35 | -------------------------------------------------------------------------------- /volume/testing_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/volume" 11 | ) 12 | 13 | func TestCleanup(t *testing.T) { 14 | t.Run("cleanup-by-id-nonexistent", func(t *testing.T) { 15 | // Tests ErrNotFound case - should be cleanup safe 16 | volume.CleanupByID(t, "nonexistent-volume-id") 17 | }) 18 | 19 | t.Run("cleanup-nil-volume", func(t *testing.T) { 20 | // Tests nil case - should be cleanup safe 21 | volume.Cleanup(t, nil) 22 | }) 23 | 24 | t.Run("concurrent-cleanups", func(t *testing.T) { 25 | // Tests ErrNotFound case after first cleanup 26 | v, err := volume.New(context.Background()) 27 | require.NoError(t, err) 28 | 29 | // ten goroutines trying to cleanup the same volume 30 | wg := sync.WaitGroup{} 31 | wg.Add(50) 32 | for i := 0; i < 50; i++ { 33 | go func() { 34 | volume.Cleanup(t, v) 35 | wg.Done() 36 | }() 37 | } 38 | wg.Wait() 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /container/lifecycle.start.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import "context" 4 | 5 | // startingHook is a hook that will be called before a container is started. 6 | func (c *Container) startingHook(ctx context.Context) error { 7 | return c.applyLifecycleHooks(ctx, true, func(lifecycleHooks LifecycleHooks) error { 8 | return applyContainerHooks(ctx, lifecycleHooks.PreStarts, c) 9 | }) 10 | } 11 | 12 | // startedHook is a hook that will be called after a container is started. 13 | func (c *Container) startedHook(ctx context.Context) error { 14 | return c.applyLifecycleHooks(ctx, true, func(lifecycleHooks LifecycleHooks) error { 15 | return applyContainerHooks(ctx, lifecycleHooks.PostStarts, c) 16 | }) 17 | } 18 | 19 | // readiedHook is a hook that will be called after a container is ready. 20 | func (c *Container) readiedHook(ctx context.Context) error { 21 | return c.applyLifecycleHooks(ctx, true, func(lifecycleHooks LifecycleHooks) error { 22 | return applyContainerHooks(ctx, lifecycleHooks.PostReadies, c) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /context/context.delete_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/docker/go-sdk/context" 9 | ) 10 | 11 | func TestDelete(t *testing.T) { 12 | context.SetupTestDockerContexts(t, 1, 3) 13 | 14 | t.Run("success", func(tt *testing.T) { 15 | ctx, err := context.New("test", context.WithHost("tcp://127.0.0.1:1234")) 16 | require.NoError(tt, err) 17 | require.NoError(tt, ctx.Delete()) 18 | 19 | list, err := context.List() 20 | require.NoError(tt, err) 21 | require.NotContains(tt, list, ctx.Name) 22 | 23 | got, err := context.Inspect(ctx.Name) 24 | require.ErrorIs(tt, err, context.ErrDockerContextNotFound) 25 | require.Empty(tt, got) 26 | }) 27 | 28 | t.Run("error/encoded-name", func(tt *testing.T) { 29 | context.SetupTestDockerContexts(tt, 1, 3) 30 | 31 | ctx := context.Context{ 32 | Name: "test", 33 | } 34 | 35 | err := ctx.Delete() 36 | require.ErrorContains(tt, err, "context has no encoded name") 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /client/testdata/certificates/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICtTCCAZ2gAwIBAgICB+MwDQYJKoZIhvcNAQELBQAwADAeFw0yNTA2MDUwODUz 3 | MThaFw0yNjA2MDUwODUzMThaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 4 | AoIBAQCloqtuzWBErSRkWp20zn6R6pAHiIVJ2C+AuuZebPjNd+4D/UhKbM+4habd 5 | 98oEBLuQsiPDTfH/kZ9G2jFPIaOwE9tGT/G6erjwKQHj3nVur4jpdiqTWIaShIq8 6 | aKlkCxLvQGoXcdGlgs913flSe79TapcTKGRMe6Xy5bJZVSXij2mGQWLuotuYqKRk 7 | UjAlWRqfqAF6x3Fm5syAJlJx5C2j586bl0a6zMkjYsTIT5jUf6rs62ZbiBR1AHHY 8 | MkFwdTb7v8gQ79ItVNdlWH6S2kW+hi6SRgutAxIIwPWD9W0N2b3Q0D9WR1u8Oa4B 9 | CPWeUMFfNq/RY1hv3As7REeYpSrvAgMBAAGjOTA3MA4GA1UdDwEB/wQEAwIFoDAM 10 | BgNVHRMBAf8EAjAAMBcGA1UdEQEB/wQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0B 11 | AQsFAAOCAQEABpWohrJX17P6MC4VsNzn93TXoTnJX4bAF6VbmAZ1rXLOsy/hR8wY 12 | l6yXacjsCvcEhR7irG5oSccURTX7Lrh6gveBxMA09Ufv+nWKM/jRiiP0te+Va12r 13 | taXDejJNB1jNAct20Q8/MJdfkSG7fQ0Xy1G44P+mQRolVOSywruqp2yudilgU8Ae 14 | IqP1VFM71Z3nt3NSHsfu7iR1wPWb4aLBNDWgkuf4MQgmr5kp9oEfZl74PPVvbytr 15 | j4/v7HcSty8PbRnwaz+XTSqFTWekyDLap2Gkc/21gahQ35ONLQ/is+niCJnHBuXK 16 | KgCB9IfR1CxhRW2+Pt2na1uGl79UOcA/Cg== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /container/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | // Validate our types implement the required interfaces. 9 | var ( 10 | _ Logger = (*log.Logger)(nil) 11 | _ Logger = (*noopLogger)(nil) 12 | ) 13 | 14 | // Logger defines the Logger interface. 15 | type Logger interface { 16 | Printf(format string, v ...any) 17 | } 18 | 19 | // defaultLogger is the default Logger instance. 20 | var defaultLogger Logger = log.New(os.Stderr, "", log.LstdFlags) 21 | 22 | // NoopLogger is a Logger that does nothing. 23 | var NoopLogger Logger = &noopLogger{} 24 | 25 | // Default returns the default Logger instance. 26 | func Default() Logger { 27 | return defaultLogger 28 | } 29 | 30 | // SetDefault sets the default Logger instance. 31 | func SetDefault(l Logger) { 32 | defaultLogger = l 33 | } 34 | 35 | // Printf implements Logging. 36 | func Printf(format string, v ...any) { 37 | defaultLogger.Printf(format, v...) 38 | } 39 | 40 | type noopLogger struct{} 41 | 42 | // Printf implements Logging. 43 | func (n noopLogger) Printf(_ string, _ ...any) { 44 | // NOOP 45 | } 46 | -------------------------------------------------------------------------------- /image/options_test.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | dockerclient "github.com/moby/moby/client" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/client" 11 | ) 12 | 13 | func TestWithOptions(t *testing.T) { 14 | t.Run("with-pull-client", func(t *testing.T) { 15 | pullClient := &mockImagePullClient{} 16 | sdk, err := client.New(context.TODO(), client.WithDockerAPI(pullClient)) 17 | require.NoError(t, err) 18 | pullOpts := &pullOptions{} 19 | err = WithPullClient(sdk)(pullOpts) 20 | require.NoError(t, err) 21 | require.Equal(t, sdk, pullOpts.client) 22 | }) 23 | 24 | t.Run("with-pull-options", func(t *testing.T) { 25 | opts := dockerclient.ImagePullOptions{} 26 | pullOpts := &pullOptions{} 27 | err := WithPullOptions(opts)(pullOpts) 28 | require.NoError(t, err) 29 | require.Equal(t, opts, pullOpts.pullOptions) 30 | }) 31 | 32 | t.Run("with-credentials-from-config", func(t *testing.T) { 33 | opts := &pullOptions{} 34 | err := WithCredentialsFromConfig(opts) 35 | require.NoError(t, err) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /client/labels.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "maps" 4 | 5 | const ( 6 | // LabelBase is the base label for all Docker SDK labels. 7 | LabelBase = "com.docker.sdk" 8 | 9 | // LabelLang specifies the language. 10 | LabelLang = LabelBase + ".lang" 11 | 12 | // LabelVersion specifies the version of go-sdk's client. 13 | LabelVersion = LabelBase + ".client" 14 | ) 15 | 16 | // sdkLabels is a map of labels that can be used to identify resources 17 | // created by this library. 18 | var sdkLabels = map[string]string{ 19 | LabelBase: "true", 20 | LabelLang: "go", 21 | LabelVersion: Version(), 22 | } 23 | 24 | // AddSDKLabels adds the SDK labels to target. 25 | func AddSDKLabels(target map[string]string) { 26 | if target == nil { 27 | target = make(map[string]string) 28 | } 29 | maps.Copy(target, sdkLabels) 30 | } 31 | 32 | // SDKLabels returns a map of labels that can be used to identify resources 33 | // created by this library. 34 | func SDKLabels() map[string]string { 35 | return map[string]string{ 36 | LabelBase: "true", 37 | LabelLang: "go", 38 | LabelVersion: Version(), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## What does this PR do? 6 | 7 | 10 | 11 | ## Why is it important? 12 | 13 | 16 | 17 | ## Related issues 18 | 19 | 28 | 29 | 34 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /client/testdata/certificates/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC1zCCAb+gAwIBAgICB+MwDQYJKoZIhvcNAQELBQAwADAeFw0yNTA2MDUwODUz 3 | MThaFw0yNjA2MDUwODUzMThaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 4 | AoIBAQC5DofaT+phjDcbr8G2NN1CKlx0gycO4tZbC2i2R9ih7wdj61nKrpOFYAw2 5 | 7EGnsCU5Lelz3QLic2IsNo+RtOz4pgtWIwK1NL6RICXsGt+LzGREWvpjUo5ZiRV7 6 | BU+MLeqyBjATPRfjemQSaL3DXnQfZ0QPnKphI5usLQZd7BZh38KL/zbWE7KcMrCN 7 | +wR0daS8WqJKJIUa9D3c/6EoJdmELMVcf5jfUxO49nUDNlX2OnLjKgqltpMQQgob 8 | ReO9zLDiy+4SqbuvHgLkEq0oV+0txnx8n9JZ4j1FrbF93/YegDN58PwYUFfpsZAQ 9 | QCAJuD8J9c+EKReIyQs/C8AcUblJAgMBAAGjWzBZMA4GA1UdDwEB/wQEAwICpDAP 10 | BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRjCIg9fVlKV/U+wSVOqib6xOnOvzAX 11 | BgNVHREBAf8EDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBACdrY7Ia 12 | SGbD58zOiX1KfOQ0GRkYjTlx/q4ADMTorKM5VxkD6kVsRyhF3HOyv9VNDtLjzidY 13 | 35Cc2DYpUagb6MfTMYuD4+WGe5Zge3GNxQLzmDrqtmw1CYjH0f7aH426Ss2+7OTc 14 | 8wzADqt8Ugh865+zD+oWPMc2zPdrpKCuah419wXRKk7/QM9Wa0puiEfaXEgIxY6W 15 | QueoISrw60bUYsKzgZ5ufNkqLu2oRzqfuYpBeLiGWWpOzZ4hmpB+UosbaINNgvFD 16 | BCQjRIpJbeqBir4qOAIPDnsI9aDSGgfI9MZdXe1GQxeokKvDwItzpU8gXbd3IAgU 17 | cyvnM9sACmunLZ4= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /image/remove.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | dockerclient "github.com/moby/moby/client" 9 | 10 | "github.com/docker/go-sdk/client" 11 | ) 12 | 13 | // Remove removes an image from the local repository. 14 | func Remove(ctx context.Context, image string, opts ...RemoveOption) (dockerclient.ImageRemoveResult, error) { 15 | removeOpts := &removeOptions{} 16 | for _, opt := range opts { 17 | if err := opt(removeOpts); err != nil { 18 | return dockerclient.ImageRemoveResult{}, fmt.Errorf("apply remove option: %w", err) 19 | } 20 | } 21 | 22 | if image == "" { 23 | return dockerclient.ImageRemoveResult{}, errors.New("image is required") 24 | } 25 | 26 | if removeOpts.client == nil { 27 | sdk, err := client.New(ctx) 28 | if err != nil { 29 | return dockerclient.ImageRemoveResult{}, err 30 | } 31 | removeOpts.client = sdk 32 | } 33 | 34 | resp, err := removeOpts.client.ImageRemove(ctx, image, removeOpts.removeOptions) 35 | if err != nil { 36 | return dockerclient.ImageRemoveResult{}, fmt.Errorf("remove image: %w", err) 37 | } 38 | 39 | return resp, nil 40 | } 41 | -------------------------------------------------------------------------------- /context/context.delete.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/docker/go-sdk/config" 8 | ) 9 | 10 | // Delete deletes a context. The context must exist: it must have been created with [New] 11 | // or inspected with [Inspect]. 12 | // If the context is the default context, the current context will be reset to the default context. 13 | func (ctx *Context) Delete() error { 14 | if ctx.encodedName == "" { 15 | return errors.New("context has no encoded name") 16 | } 17 | 18 | metaRoot, err := metaRoot() 19 | if err != nil { 20 | return fmt.Errorf("meta root: %w", err) 21 | } 22 | 23 | s := &store{root: metaRoot} 24 | 25 | err = s.delete(ctx.encodedName) 26 | if err != nil { 27 | return fmt.Errorf("delete: %w", err) 28 | } 29 | 30 | if ctx.isCurrent { 31 | // reset the current context to the default context 32 | cfg, err := config.Load() 33 | if err != nil { 34 | return fmt.Errorf("load config: %w", err) 35 | } 36 | 37 | cfg.CurrentContext = DefaultContextName 38 | 39 | if err := cfg.Save(); err != nil { 40 | return fmt.Errorf("save config: %w", err) 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /third_party/cpuguy83/dockercfg/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brian Goff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /context/rootless_test.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRootlessSocketPathFromEnv(t *testing.T) { 12 | t.Run("success", func(t *testing.T) { 13 | tmpDir := t.TempDir() 14 | t.Setenv("XDG_RUNTIME_DIR", tmpDir) 15 | 16 | err := os.WriteFile(filepath.Join(tmpDir, "docker.sock"), []byte("synthetic docker socket"), 0o755) 17 | require.NoError(t, err) 18 | 19 | path, err := rootlessSocketPathFromEnv() 20 | require.NoError(t, err) 21 | require.Equal(t, DefaultSchema+filepath.Join(tmpDir, "docker.sock"), path) 22 | }) 23 | 24 | t.Run("env-var-not-set", func(t *testing.T) { 25 | t.Setenv("XDG_RUNTIME_DIR", "") 26 | path, err := rootlessSocketPathFromEnv() 27 | require.ErrorIs(t, err, ErrXDGRuntimeDirNotSet) 28 | require.Empty(t, path) 29 | }) 30 | 31 | t.Run("docker-socket-not-found", func(t *testing.T) { 32 | tmpDir := t.TempDir() 33 | t.Setenv("XDG_RUNTIME_DIR", tmpDir) 34 | 35 | path, err := rootlessSocketPathFromEnv() 36 | require.ErrorIs(t, err, ErrRootlessDockerNotFoundXDGRuntimeDir) 37 | require.Empty(t, path) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /image/build.log.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "encoding/json" 5 | "log/slog" 6 | "strings" 7 | 8 | "github.com/moby/moby/api/types/jsonstream" 9 | ) 10 | 11 | // loggerWriter is a custom writer that forwards to the slog.Logger 12 | type loggerWriter struct { 13 | logger *slog.Logger 14 | } 15 | 16 | // Write writes the message to the logger. 17 | func (lw *loggerWriter) Write(p []byte) (int, error) { 18 | // Try to parse as JSON message first 19 | var msg jsonstream.Message 20 | if err := json.Unmarshal(p, &msg); err == nil { 21 | // It's a JSON message, log it structured and there is no default case because 22 | // empty JSON messages should not be logged, to avoid noise. 23 | switch { 24 | case msg.Error != nil: 25 | lw.logger.Error("Build error", "error", msg.Error.Message) 26 | case msg.Stream != "": 27 | lw.logger.Info(strings.TrimSuffix(msg.Stream, "\n")) 28 | case msg.Status != "": 29 | lw.logger.Info(msg.Status, "id", msg.ID, "progress", msg.Progress) 30 | } 31 | } else { 32 | // Fall back to plain text 33 | text := strings.TrimSuffix(string(p), "\n") 34 | if text != "" { 35 | lw.logger.Info(text) 36 | } 37 | } 38 | return len(p), nil 39 | } 40 | -------------------------------------------------------------------------------- /network/network.terminate.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/moby/moby/client" 10 | ) 11 | 12 | // TerminableNetwork is a network that can be terminated. 13 | type TerminableNetwork interface { 14 | Terminate(ctx context.Context) error 15 | } 16 | 17 | // Terminate is used to remove the network. It is usually triggered by as defer function. 18 | func (n *Network) Terminate(ctx context.Context) error { 19 | if n.dockerClient == nil { 20 | return errors.New("docker client is not initialized") 21 | } 22 | 23 | if _, err := n.dockerClient.NetworkRemove(ctx, n.ID(), client.NetworkRemoveOptions{}); err != nil { 24 | return fmt.Errorf("terminate network: %w", err) 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // isNil returns true if val is nil or a nil instance false otherwise. 31 | func isNil(val any) bool { 32 | if val == nil { 33 | return true 34 | } 35 | 36 | valueOf := reflect.ValueOf(val) 37 | switch valueOf.Kind() { 38 | case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: 39 | return valueOf.IsNil() 40 | default: 41 | return false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /third_party/testcontainers/testcontainers-go/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2019 Gianluca Arbezzano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Function to execute a command in all modules 2 | define for-all-modules 3 | @go work edit -json | jq -r '.Use[].DiskPath' | while read -r module; do \ 4 | echo "Processing module: $$module"; \ 5 | (cd "$$module" && $(1)) || exit 1; \ 6 | done 7 | endef 8 | 9 | # Run make lint in all modules defined in go.work 10 | lint-all: 11 | @echo "Running lint in all modules..." 12 | $(call for-all-modules,make lint) 13 | 14 | tidy-all: 15 | @echo "Running tidy in all modules..." 16 | $(call for-all-modules,go mod tidy) 17 | 18 | clean-build-dir: 19 | @echo "Cleaning build directory..." 20 | @rm -rf .github/scripts/.build 21 | @mkdir -p .github/scripts/.build 22 | 23 | # Pre-release version for all modules 24 | pre-release-all: clean-build-dir 25 | @echo "Preparing releasing versions for all modules..." 26 | $(call for-all-modules,make pre-release) 27 | 28 | # Release version for all modules. It must be run after pre-release-all. 29 | release-all: 30 | $(call for-all-modules,make check-pre-release) 31 | @./.github/scripts/release.sh 32 | 33 | # Refresh Go proxy for all modules 34 | refresh-proxy-all: 35 | @echo "Refreshing Go proxy for all modules..." 36 | $(call for-all-modules,make refresh-proxy) 37 | -------------------------------------------------------------------------------- /volume/volume.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | dockerclient "github.com/moby/moby/client" 8 | 9 | "github.com/docker/go-sdk/client" 10 | ) 11 | 12 | // New creates a new volume. 13 | // If no name is provided, a random name is generated. 14 | // If no client is provided, the default client is used. 15 | func New(ctx context.Context, opts ...Option) (*Volume, error) { 16 | volumeOptions := &options{ 17 | labels: make(map[string]string), 18 | } 19 | 20 | for _, opt := range opts { 21 | if err := opt(volumeOptions); err != nil { 22 | return nil, fmt.Errorf("apply option: %w", err) 23 | } 24 | } 25 | 26 | if volumeOptions.client == nil { 27 | sdk, err := client.New(ctx) 28 | if err != nil { 29 | return nil, err 30 | } 31 | volumeOptions.client = sdk 32 | } 33 | 34 | volumeOptions.labels[moduleLabel] = Version() 35 | 36 | v, err := volumeOptions.client.VolumeCreate(ctx, dockerclient.VolumeCreateOptions{ 37 | Name: volumeOptions.name, 38 | Labels: volumeOptions.labels, 39 | }) 40 | if err != nil { 41 | return nil, fmt.Errorf("create volume: %w", err) 42 | } 43 | 44 | return &Volume{ 45 | Volume: &v.Volume, 46 | dockerClient: volumeOptions.client, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /client/config_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_newConfig(t *testing.T) { 11 | t.Run("success", func(t *testing.T) { 12 | cfg, err := newConfig("docker-host") 13 | require.NoError(t, err) 14 | require.Equal(t, "docker-host", cfg.Host) 15 | require.False(t, cfg.TLSVerify) 16 | require.Empty(t, cfg.CertPath) 17 | }) 18 | 19 | t.Run("success/tls-verify", func(t *testing.T) { 20 | certDir := filepath.Join("testdata", "certificates") 21 | 22 | t.Setenv("DOCKER_TLS_VERIFY", "1") 23 | t.Setenv("DOCKER_CERT_PATH", certDir) 24 | 25 | cfg, err := newConfig("docker-host") 26 | require.NoError(t, err) 27 | require.Equal(t, "docker-host", cfg.Host) 28 | require.True(t, cfg.TLSVerify) 29 | require.Equal(t, certDir, cfg.CertPath) 30 | }) 31 | 32 | t.Run("error/host-required", func(t *testing.T) { 33 | cfg, err := newConfig("") 34 | require.Error(t, err) 35 | require.Nil(t, cfg) 36 | }) 37 | 38 | t.Run("error/cert-path-required-for-tls", func(t *testing.T) { 39 | t.Setenv("DOCKER_TLS_VERIFY", "1") 40 | 41 | cfg, err := newConfig("docker-host") 42 | require.Error(t, err) 43 | require.Nil(t, cfg) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /context/rootless.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | var ( 10 | ErrRootlessDockerNotFoundXDGRuntimeDir = errors.New("docker.sock not found in $XDG_RUNTIME_DIR") 11 | ErrXDGRuntimeDirNotSet = errors.New("$XDG_RUNTIME_DIR is not set") 12 | ErrInvalidSchema = errors.New("URL schema is not " + DefaultSchema + " or tcp") 13 | ) 14 | 15 | // rootlessSocketPathFromEnv returns the path to the rootless Docker socket from the XDG_RUNTIME_DIR environment variable. 16 | // It should include the Docker socket schema (unix://, npipe:// or tcp://) in the returned path. 17 | func rootlessSocketPathFromEnv() (string, error) { 18 | xdgRuntimeDir, exists := os.LookupEnv("XDG_RUNTIME_DIR") 19 | if exists && xdgRuntimeDir != "" { 20 | f := filepath.Join(xdgRuntimeDir, "docker.sock") 21 | if fileExists(f) { 22 | return DefaultSchema + f, nil 23 | } 24 | 25 | return "", ErrRootlessDockerNotFoundXDGRuntimeDir 26 | } 27 | 28 | return "", ErrXDGRuntimeDirNotSet 29 | } 30 | 31 | // fileExists checks if a file exists. 32 | func fileExists(path string) bool { 33 | if _, err := os.Stat(path); os.IsNotExist(err) { 34 | return false 35 | } 36 | 37 | return true 38 | } 39 | -------------------------------------------------------------------------------- /container/ports_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package container_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/moby/moby/api/types/network" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/container" 11 | ) 12 | 13 | func BenchmarkPortEndpoint(b *testing.B) { 14 | ctr, err := container.Run(context.Background(), 15 | container.WithImage(nginxAlpineImage), 16 | ) 17 | container.Cleanup(b, ctr) 18 | require.NoError(b, err) 19 | require.NotNil(b, ctr) 20 | 21 | port80 := network.MustParsePort("80/tcp") 22 | 23 | b.Run("port-endpoint", func(b *testing.B) { 24 | b.ReportAllocs() 25 | b.ResetTimer() 26 | for i := 0; i < b.N; i++ { 27 | _, err := ctr.PortEndpoint(context.Background(), port80, "tcp") 28 | require.NoError(b, err) 29 | } 30 | }) 31 | 32 | b.Run("mapped-port", func(b *testing.B) { 33 | b.ReportAllocs() 34 | b.ResetTimer() 35 | for i := 0; i < b.N; i++ { 36 | _, err := ctr.MappedPort(context.Background(), port80) 37 | require.NoError(b, err) 38 | } 39 | }) 40 | 41 | b.Run("endpoint", func(b *testing.B) { 42 | b.ReportAllocs() 43 | b.ResetTimer() 44 | for i := 0; i < b.N; i++ { 45 | _, err := ctr.Endpoint(context.Background(), "tcp") 46 | require.NoError(b, err) 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /container/container.terminate_test.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestTerminateOptions(t *testing.T) { 12 | t.Run("with-stop-timeout", func(t *testing.T) { 13 | opts := NewTerminateOptions(context.Background(), TerminateTimeout(10*time.Second)) 14 | require.Equal(t, 10*time.Second, opts.StopTimeout()) 15 | }) 16 | 17 | t.Run("with-volumes", func(t *testing.T) { 18 | opts := NewTerminateOptions(context.Background(), RemoveVolumes("vol1", "vol2")) 19 | require.Equal(t, []string{"vol1", "vol2"}, opts.volumes) 20 | }) 21 | 22 | t.Run("with-stop-timeout-and-volumes", func(t *testing.T) { 23 | opts := NewTerminateOptions(context.Background(), TerminateTimeout(10*time.Second), RemoveVolumes("vol1", "vol2")) 24 | require.Equal(t, 10*time.Second, opts.StopTimeout()) 25 | require.Equal(t, []string{"vol1", "vol2"}, opts.volumes) 26 | }) 27 | 28 | t.Run("with-stop-timeout-and-volumes-and-context", func(t *testing.T) { 29 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 30 | defer cancel() 31 | opts := NewTerminateOptions(ctx, TerminateTimeout(10*time.Second), RemoveVolumes("vol1", "vol2")) 32 | require.Equal(t, ctx, opts.Context()) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /container/inspect.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/moby/moby/api/types/container" 7 | "github.com/moby/moby/client" 8 | ) 9 | 10 | // Inspect returns the container's raw info 11 | func (c *Container) Inspect(ctx context.Context) (client.ContainerInspectResult, error) { 12 | inspect, err := c.dockerClient.ContainerInspect(ctx, c.ID(), client.ContainerInspectOptions{}) 13 | if err != nil { 14 | return client.ContainerInspectResult{}, err 15 | } 16 | 17 | return inspect, nil 18 | } 19 | 20 | // InspectWithOptions returns the container's raw info, passing custom options. 21 | // 22 | // This method may be deprecated in the near future, to be replaced by functional options for Inspect. 23 | func (c *Container) InspectWithOptions(ctx context.Context, options client.ContainerInspectOptions) (client.ContainerInspectResult, error) { 24 | inspect, err := c.dockerClient.ContainerInspect(ctx, c.ID(), options) 25 | if err != nil { 26 | return client.ContainerInspectResult{}, err 27 | } 28 | 29 | return inspect, nil 30 | } 31 | 32 | // State returns container's running state. 33 | func (c *Container) State(ctx context.Context) (*container.State, error) { 34 | inspect, err := c.Inspect(ctx) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return inspect.Container.State, nil 40 | } 41 | -------------------------------------------------------------------------------- /client/client.container.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/containerd/errdefs" 8 | "github.com/moby/moby/api/types/container" 9 | "github.com/moby/moby/client" 10 | ) 11 | 12 | // ContainerCreate creates a new container. 13 | func (c *sdkClient) ContainerCreate(ctx context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { 14 | // Add the labels that identify this as a container created by the SDK. 15 | AddSDKLabels(options.Config.Labels) 16 | 17 | return c.APIClient.ContainerCreate(ctx, options) 18 | } 19 | 20 | // FindContainerByName finds a container by name. The name filter uses a regex to find the containers. 21 | func (c *sdkClient) FindContainerByName(ctx context.Context, name string) (*container.Summary, error) { 22 | if name == "" { 23 | return nil, errdefs.ErrInvalidArgument.WithMessage("name is empty") 24 | } 25 | 26 | // Note that, 'name' filter will use regex to find the containers 27 | containers, err := c.ContainerList(ctx, client.ContainerListOptions{ 28 | All: true, 29 | Filters: make(client.Filters).Add("name", "^"+name+"$"), 30 | }) 31 | if err != nil { 32 | return nil, fmt.Errorf("container list: %w", err) 33 | } 34 | 35 | if len(containers.Items) > 0 { 36 | return &containers.Items[0], nil 37 | } 38 | 39 | return nil, errdefs.ErrNotFound.WithMessage(fmt.Sprintf("container %s not found", name)) 40 | } 41 | -------------------------------------------------------------------------------- /client/options_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log/slog" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestWithOptions(t *testing.T) { 13 | t.Run("success", func(t *testing.T) { 14 | t.Run("docker-host", func(t *testing.T) { 15 | cli := &sdkClient{} 16 | require.NoError(t, WithDockerHost("tcp://localhost:2375").Apply(cli)) 17 | require.Equal(t, "tcp://localhost:2375", cli.dockerHost) 18 | }) 19 | 20 | t.Run("docker-context", func(t *testing.T) { 21 | cli := &sdkClient{} 22 | require.NoError(t, WithDockerContext("test-context").Apply(cli)) 23 | require.Equal(t, "test-context", cli.dockerContext) 24 | }) 25 | 26 | t.Run("extra-headers", func(t *testing.T) { 27 | cli := &sdkClient{} 28 | require.NoError(t, WithExtraHeaders(map[string]string{"X-Test": "test"}).Apply(cli)) 29 | require.Equal(t, map[string]string{"X-Test": "test"}, cli.extraHeaders) 30 | }) 31 | 32 | t.Run("health-check", func(t *testing.T) { 33 | cli := &sdkClient{} 34 | require.NoError(t, WithHealthCheck(func(_ context.Context) func(_ SDKClient) error { 35 | return nil 36 | }).Apply(cli)) 37 | require.NotNil(t, cli.healthCheck) 38 | }) 39 | 40 | t.Run("logger", func(t *testing.T) { 41 | cli := &sdkClient{} 42 | require.NoError(t, WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))).Apply(cli)) 43 | require.NotNil(t, cli.log) 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /.github/release-drafter-template.yml: -------------------------------------------------------------------------------- 1 | name-template: '{{FOLDER}} - v$RESOLVED_VERSION' 2 | tag-template: '{{FOLDER}}/v$RESOLVED_VERSION' 3 | tag-prefix: '{{FOLDER}}/v' 4 | include-paths: 5 | - {{FOLDER}} 6 | categories: 7 | - title: ⚠️ Breaking Changes 8 | labels: 9 | - 'breaking change' 10 | - title: '🚀 New Features' 11 | labels: 12 | - 'enhancement' 13 | - 'feature' 14 | - title: '🐛 Bug Fixes' 15 | labels: 16 | - 'bug' 17 | - title: '📚 Documentation' 18 | labels: 19 | - 'documentation' 20 | - title: 🧹 Housekeeping 21 | labels: 22 | - 'chore' 23 | - title: 📦 Dependency updates 24 | label: 'dependencies' 25 | change-template: '- $TITLE (#$NUMBER)' 26 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 27 | exclude-contributors: 28 | - dependabot 29 | - dependabot[bot] 30 | version-resolver: 31 | major: 32 | labels: 33 | - 'major' 34 | - '❗ BreakingChange' 35 | minor: 36 | labels: 37 | - 'minor' 38 | - '✏️ Feature' 39 | patch: 40 | labels: 41 | - 'patch' 42 | - '📒 Documentation' 43 | - '🐞 Bug' 44 | - '🤖 Dependencies' 45 | - '🔧 Updates' 46 | default: patch 47 | template: | 48 | $CHANGES 49 | 50 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...{{FOLDER}}/v$RESOLVED_VERSION 51 | 52 | Thank you $CONTRIBUTORS for making this update possible. 53 | -------------------------------------------------------------------------------- /context/options.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | // contextOptions is the options for creating a context. 4 | type contextOptions struct { 5 | host string 6 | description string 7 | additionalFields map[string]any 8 | skipTLSVerify bool 9 | current bool 10 | } 11 | 12 | // CreateContextOption is a function that can be used to create a context. 13 | type CreateContextOption func(*contextOptions) error 14 | 15 | // WithHost sets the host for the context. 16 | func WithHost(host string) CreateContextOption { 17 | return func(c *contextOptions) error { 18 | c.host = host 19 | return nil 20 | } 21 | } 22 | 23 | // WithDescription sets the description for the context. 24 | func WithDescription(description string) CreateContextOption { 25 | return func(c *contextOptions) error { 26 | c.description = description 27 | return nil 28 | } 29 | } 30 | 31 | // WithAdditionalFields sets the additional fields for the context. 32 | func WithAdditionalFields(fields map[string]any) CreateContextOption { 33 | return func(c *contextOptions) error { 34 | c.additionalFields = fields 35 | return nil 36 | } 37 | } 38 | 39 | // WithSkipTLSVerify sets the skipTLSVerify flag to true. 40 | func WithSkipTLSVerify() CreateContextOption { 41 | return func(c *contextOptions) error { 42 | c.skipTLSVerify = true 43 | return nil 44 | } 45 | } 46 | 47 | // AsCurrent sets the context as the current context. 48 | func AsCurrent() CreateContextOption { 49 | return func(c *contextOptions) error { 50 | c.current = true 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Docker Client 2 | 3 | This package provides a client for the Docker API. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/docker/go-sdk/client 9 | ``` 10 | 11 | ## Usage 12 | 13 | The library provides a default client that is initialised with the current docker context. It uses a default logger that is configured to print to the standard output using the `slog` package. 14 | 15 | ```go 16 | cli := client.DefaultClient 17 | ``` 18 | 19 | It's also possible to create a new client, with optional configuration: 20 | 21 | ```go 22 | cli, err := client.New(context.Background()) 23 | if err != nil { 24 | log.Fatalf("failed to create docker client: %v", err) 25 | } 26 | 27 | // Close the docker client when done 28 | defer cli.Close() 29 | ``` 30 | 31 | ## Customizing the client 32 | 33 | The client created with the `New` function can be customized using functional options. The following options are available: 34 | 35 | - `WithHealthCheck(healthCheck func(ctx context.Context) func(c *Client) error) ClientOption`: A healthcheck function that is called to check the health of the client. By default, the client uses `Ping` to check the health of the client. 36 | - `WithDockerHost(dockerHost string) ClientOption`: The docker host to use. By default, the client uses the current docker host. 37 | - `WithDockerContext(dockerContext string) ClientOption`: The docker context to use. By default, the client uses the current docker context. 38 | 39 | In the case that both the docker host and the docker context are provided, the docker context takes precedence. 40 | -------------------------------------------------------------------------------- /container/container.from_test.go: -------------------------------------------------------------------------------- 1 | package container_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/docker/go-sdk/container" 12 | ) 13 | 14 | func TestFromID(t *testing.T) { 15 | // First, create a container using Run 16 | ctr, err := container.Run(context.Background(), container.WithImage("alpine:latest")) 17 | require.NoError(t, err) 18 | 19 | // Use the SDK client from the existing container 20 | cli := ctr.Client() 21 | 22 | // Now recreate the container using FromID with the container ID 23 | // This is useful when you only have a container ID and need to perform operations on it 24 | recreated, err := container.FromID(context.Background(), cli, ctr.ID()) 25 | require.NoError(t, err) 26 | require.Equal(t, ctr.ID(), recreated.ID()) 27 | 28 | // Verify operations like CopyToContainer on the recreated container 29 | content := []byte("Hello from FromID!") 30 | require.NoError(t, recreated.CopyToContainer(context.Background(), content, "/tmp/test.txt", 0o644)) 31 | 32 | rc, err := recreated.CopyFromContainer(context.Background(), "/tmp/test.txt") 33 | require.NoError(t, err) 34 | 35 | buf := new(bytes.Buffer) 36 | _, err = io.Copy(buf, rc) 37 | require.NoError(t, err) 38 | require.Equal(t, string(content), buf.String()) 39 | 40 | // Terminate the recreated container 41 | err = recreated.Terminate(context.Background()) 42 | require.NoError(t, err) 43 | 44 | // Terminate the original container should fail 45 | err = ctr.Terminate(context.Background()) 46 | require.Error(t, err) 47 | } 48 | -------------------------------------------------------------------------------- /image/remove_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | dockerclient "github.com/moby/moby/client" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/client" 11 | "github.com/docker/go-sdk/image" 12 | ) 13 | 14 | func TestRemove(t *testing.T) { 15 | img := "redis:alpine" 16 | 17 | t.Run("success", func(t *testing.T) { 18 | pullImage(t, img) 19 | 20 | resp, err := image.Remove(context.Background(), img) 21 | require.NoError(t, err) 22 | require.NotEmpty(t, resp) 23 | }) 24 | 25 | t.Run("success/with-client", func(t *testing.T) { 26 | pullImage(t, img) 27 | 28 | dockerClient, err := client.New(context.Background()) 29 | require.NoError(t, err) 30 | 31 | resp, err := image.Remove(context.Background(), img, image.WithRemoveClient(dockerClient)) 32 | require.NoError(t, err) 33 | require.NotEmpty(t, resp) 34 | }) 35 | 36 | t.Run("success/with-options", func(t *testing.T) { 37 | pullImage(t, img) 38 | 39 | resp, err := image.Remove(context.Background(), img, image.WithRemoveOptions(dockerclient.ImageRemoveOptions{ 40 | Force: true, 41 | PruneChildren: true, 42 | })) 43 | require.NoError(t, err) 44 | require.NotEmpty(t, resp) 45 | }) 46 | 47 | t.Run("error/blank-image", func(t *testing.T) { 48 | pullImage(t, img) 49 | 50 | resp, err := image.Remove(context.Background(), "") 51 | require.Error(t, err) 52 | require.Empty(t, resp) 53 | }) 54 | } 55 | 56 | func pullImage(t *testing.T, img string) { 57 | t.Helper() 58 | 59 | err := image.Pull(context.Background(), img) 60 | require.NoError(t, err) 61 | } 62 | -------------------------------------------------------------------------------- /config/auth.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/moby/moby/api/pkg/authconfig" 7 | "github.com/moby/moby/api/types/registry" 8 | ) 9 | 10 | // This is used by the docker CLI in cases where an oauth identity token is used. 11 | // In that case the username is stored literally as `` 12 | // When fetching the credentials we check for this value to determine if. 13 | const tokenUsername = "" 14 | 15 | // AuthConfigs returns the auth configs for the given images. 16 | // The images slice must contain images that are used in a Dockerfile. 17 | // The returned map is keyed by the registry registry hostname for each image. 18 | func AuthConfigs(images ...string) (map[string]registry.AuthConfig, error) { 19 | cfg, err := Load() 20 | if err != nil { 21 | return nil, fmt.Errorf("load config: %w", err) 22 | } 23 | 24 | return cfg.AuthConfigsForImages(images) 25 | } 26 | 27 | // AuthConfigForHostname gets registry credentials for the passed in registry host. 28 | // 29 | // This will use [Load] to read registry auth details from the config. 30 | // If the config doesn't exist, it will attempt to load registry credentials using the default credential helper for the platform. 31 | func AuthConfigForHostname(hostname string) (registry.AuthConfig, error) { 32 | cfg, err := Load() 33 | if err != nil { 34 | return registry.AuthConfig{}, fmt.Errorf("load config: %w", err) 35 | } 36 | 37 | return cfg.AuthConfigForHostname(hostname) 38 | } 39 | 40 | // EncodeBase64 encodes an AuthConfig into base64. 41 | func EncodeBase64(authConfig registry.AuthConfig) (string, error) { 42 | return authconfig.Encode(authConfig) 43 | } 44 | -------------------------------------------------------------------------------- /image/save.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | dockerclient "github.com/moby/moby/client" 10 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 11 | 12 | "github.com/docker/go-sdk/client" 13 | ) 14 | 15 | // Save saves an image to a file. 16 | func Save(ctx context.Context, output string, img string, opts ...SaveOption) error { 17 | saveOpts := &saveOptions{ 18 | platforms: []ocispec.Platform{}, 19 | } 20 | for _, opt := range opts { 21 | if err := opt(saveOpts); err != nil { 22 | return fmt.Errorf("apply save option: %w", err) 23 | } 24 | } 25 | 26 | if output == "" { 27 | return errors.New("output is not set") 28 | } 29 | if img == "" { 30 | return errors.New("image cannot be empty") 31 | } 32 | 33 | if saveOpts.client == nil { 34 | sdk, err := client.New(ctx) 35 | if err != nil { 36 | return err 37 | } 38 | saveOpts.client = sdk 39 | } 40 | 41 | outputFile, err := os.Create(output) 42 | if err != nil { 43 | return fmt.Errorf("open output file %w", err) 44 | } 45 | defer func() { 46 | _ = outputFile.Close() 47 | }() 48 | 49 | imgSaveOpts := dockerclient.ImageSaveWithPlatforms(saveOpts.platforms...) 50 | 51 | imageReader, err := saveOpts.client.ImageSave(ctx, []string{img}, imgSaveOpts) 52 | if err != nil { 53 | return fmt.Errorf("save images %w", err) 54 | } 55 | defer func() { 56 | _ = imageReader.Close() 57 | }() 58 | 59 | // Attempt optimized readFrom, implemented in linux 60 | _, err = outputFile.ReadFrom(imageReader) 61 | if err != nil { 62 | return fmt.Errorf("write images to output %w", err) 63 | } 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /client/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/client 2 | 3 | go 1.24.0 4 | 5 | replace ( 6 | github.com/docker/go-sdk/config => ../config 7 | github.com/docker/go-sdk/context => ../context 8 | ) 9 | 10 | require ( 11 | github.com/caarlos0/env/v11 v11.3.1 12 | github.com/containerd/errdefs v1.0.0 13 | github.com/docker/go-sdk/context v0.1.0-alpha011 14 | github.com/moby/moby/api v1.52.0 15 | github.com/moby/moby/client v0.1.0 16 | github.com/stretchr/testify v1.10.0 17 | ) 18 | 19 | require ( 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/distribution/reference v0.6.0 // indirect 24 | github.com/docker/go-connections v0.6.0 // indirect 25 | github.com/docker/go-sdk/config v0.1.0-alpha011 // indirect 26 | github.com/docker/go-units v0.5.0 // indirect 27 | github.com/felixge/httpsnoop v1.0.4 // indirect 28 | github.com/go-logr/logr v1.4.3 // indirect 29 | github.com/go-logr/stdr v1.2.2 // indirect 30 | github.com/moby/docker-image-spec v1.3.1 // indirect 31 | github.com/opencontainers/go-digest v1.0.0 // indirect 32 | github.com/opencontainers/image-spec v1.1.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 35 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 36 | go.opentelemetry.io/otel v1.37.0 // indirect 37 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 38 | go.opentelemetry.io/otel/sdk v1.37.0 // indirect 39 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 40 | golang.org/x/sys v0.33.0 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | dockerclient "github.com/moby/moby/client" 9 | 10 | "github.com/docker/go-sdk/client" 11 | ) 12 | 13 | // New creates a new network. 14 | func New(ctx context.Context, opts ...Option) (*Network, error) { 15 | networkOptions := &options{ 16 | labels: make(map[string]string), 17 | } 18 | 19 | for _, opt := range opts { 20 | if err := opt(networkOptions); err != nil { 21 | return nil, fmt.Errorf("apply option: %w", err) 22 | } 23 | } 24 | 25 | if networkOptions.name == "" { 26 | networkOptions.name = uuid.New().String() 27 | } 28 | 29 | if networkOptions.client == nil { 30 | sdk, err := client.New(ctx) 31 | if err != nil { 32 | return nil, err 33 | } 34 | networkOptions.client = sdk 35 | } 36 | 37 | networkOptions.labels[moduleLabel] = Version() 38 | 39 | nc := dockerclient.NetworkCreateOptions{ 40 | Driver: networkOptions.driver, 41 | Internal: networkOptions.internal, 42 | EnableIPv6: &networkOptions.enableIPv6, 43 | Attachable: networkOptions.attachable, 44 | Labels: networkOptions.labels, 45 | IPAM: networkOptions.ipam, 46 | } 47 | 48 | resp, err := networkOptions.client.NetworkCreate(ctx, networkOptions.name, nc) 49 | if err != nil { 50 | return nil, fmt.Errorf("create network: %w", err) 51 | } 52 | 53 | if len(resp.Warning) > 0 { 54 | networkOptions.client.Logger().Warn("warning creating network", "message", resp.Warning) 55 | } 56 | 57 | return &Network{ 58 | response: resp, 59 | name: networkOptions.name, 60 | opts: networkOptions, 61 | dockerClient: networkOptions.client, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /image/save_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/docker/go-sdk/client" 13 | "github.com/docker/go-sdk/image" 14 | ) 15 | 16 | func TestSave(t *testing.T) { 17 | img := "redis:alpine" 18 | 19 | pullImage(t, img) 20 | 21 | t.Run("success", func(t *testing.T) { 22 | output := filepath.Join(t.TempDir(), "images.tar") 23 | err := image.Save(context.Background(), output, img) 24 | require.NoError(t, err) 25 | 26 | info, err := os.Stat(output) 27 | require.NoError(t, err) 28 | 29 | require.NotZero(t, info.Size()) 30 | }) 31 | 32 | t.Run("success/with-client", func(t *testing.T) { 33 | output := filepath.Join(t.TempDir(), "images.tar") 34 | 35 | dockerClient, err := client.New(context.Background()) 36 | require.NoError(t, err) 37 | 38 | err = image.Save(context.Background(), output, img, image.WithSaveClient(dockerClient)) 39 | require.NoError(t, err) 40 | }) 41 | 42 | t.Run("success/with-platforms", func(t *testing.T) { 43 | output := filepath.Join(t.TempDir(), "images.tar") 44 | err := image.Save(context.Background(), output, img, image.WithPlatforms(ocispec.Platform{ 45 | OS: "linux", 46 | Architecture: "amd64", 47 | })) 48 | require.NoError(t, err) 49 | }) 50 | 51 | t.Run("error/no-output", func(t *testing.T) { 52 | err := image.Save(context.Background(), "", img) 53 | require.Error(t, err) 54 | }) 55 | 56 | t.Run("error/no-image", func(t *testing.T) { 57 | err := image.Save(context.Background(), filepath.Join(t.TempDir(), "images.tar"), "") 58 | require.Error(t, err) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /container/definition_test.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moby/moby/api/types/container" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestValidateMounts(t *testing.T) { 11 | t.Run("no-host-config-modifier", func(t *testing.T) { 12 | d := &Definition{} 13 | err := d.validateMounts() 14 | require.NoError(t, err) 15 | }) 16 | 17 | t.Run("invalid-bind-mount", func(t *testing.T) { 18 | d := &Definition{ 19 | hostConfigModifier: func(hc *container.HostConfig) { 20 | hc.Binds = []string{"foo"} 21 | }, 22 | } 23 | err := d.validateMounts() 24 | require.ErrorIs(t, err, ErrInvalidBindMount) 25 | }) 26 | 27 | t.Run("duplicate-mount-target", func(t *testing.T) { 28 | d := &Definition{ 29 | hostConfigModifier: func(hc *container.HostConfig) { 30 | hc.Binds = []string{"/foo:/duplicated", "/bar:/duplicated"} 31 | }, 32 | } 33 | err := d.validateMounts() 34 | require.ErrorIs(t, err, ErrDuplicateMountTarget) 35 | }) 36 | 37 | t.Run("same-source-multiple-targets", func(t *testing.T) { 38 | d := &Definition{ 39 | hostConfigModifier: func(hc *container.HostConfig) { 40 | hc.Binds = []string{"/data:/srv", "/data:/data"} 41 | }, 42 | } 43 | err := d.validateMounts() 44 | require.NoError(t, err) 45 | }) 46 | 47 | t.Run("bind-options/provided", func(t *testing.T) { 48 | d := &Definition{ 49 | hostConfigModifier: func(hc *container.HostConfig) { 50 | hc.Binds = []string{"/a:/a:nocopy", "/b:/b:ro", "/c:/c:rw", "/d:/d:z", "/e:/e:Z", "/f:/f:shared", "/g:/g:rshared", "/h:/h:slave", "/i:/i:rslave", "/j:/j:private", "/k:/k:rprivate", "/l:/l:ro,z,shared"} 51 | }, 52 | } 53 | err := d.validateMounts() 54 | require.NoError(t, err) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /volume/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/volume 2 | 3 | go 1.24.0 4 | 5 | replace ( 6 | github.com/docker/go-sdk/client => ../client 7 | github.com/docker/go-sdk/config => ../config 8 | github.com/docker/go-sdk/context => ../context 9 | ) 10 | 11 | require ( 12 | github.com/containerd/errdefs v1.0.0 13 | github.com/docker/go-sdk/client v0.1.0-alpha011 14 | github.com/moby/moby/api v1.52.0 15 | github.com/moby/moby/client v0.1.0 16 | github.com/stretchr/testify v1.10.0 17 | ) 18 | 19 | require ( 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/caarlos0/env/v11 v11.3.1 // indirect 22 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/distribution/reference v0.6.0 // indirect 25 | github.com/docker/go-connections v0.6.0 // indirect 26 | github.com/docker/go-sdk/config v0.1.0-alpha011 // indirect 27 | github.com/docker/go-sdk/context v0.1.0-alpha011 // indirect 28 | github.com/docker/go-units v0.5.0 // indirect 29 | github.com/felixge/httpsnoop v1.0.4 // indirect 30 | github.com/go-logr/logr v1.4.3 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/moby/docker-image-spec v1.3.1 // indirect 33 | github.com/opencontainers/go-digest v1.0.0 // indirect 34 | github.com/opencontainers/image-spec v1.1.1 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 37 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 38 | go.opentelemetry.io/otel v1.37.0 // indirect 39 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 40 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 41 | golang.org/x/sys v0.33.0 // indirect 42 | gopkg.in/yaml.v3 v3.0.1 // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /client/testdata/certificates/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEApaKrbs1gRK0kZFqdtM5+keqQB4iFSdgvgLrmXmz4zXfuA/1I 3 | SmzPuIWm3ffKBAS7kLIjw03x/5GfRtoxTyGjsBPbRk/xunq48CkB4951bq+I6XYq 4 | k1iGkoSKvGipZAsS70BqF3HRpYLPdd35Unu/U2qXEyhkTHul8uWyWVUl4o9phkFi 5 | 7qLbmKikZFIwJVkan6gBesdxZubMgCZSceQto+fOm5dGuszJI2LEyE+Y1H+q7Otm 6 | W4gUdQBx2DJBcHU2+7/IEO/SLVTXZVh+ktpFvoYukkYLrQMSCMD1g/VtDdm90NA/ 7 | VkdbvDmuAQj1nlDBXzav0WNYb9wLO0RHmKUq7wIDAQABAoIBAQCBVme1auu8VNMx 8 | Bc1WDVSqTlZPe5xREF3vkIIow8D4eKjENriHXTZKqRqnA0GdJ7DrCR+B91B7t1N/ 9 | eQu99c+iw66a1fw1GHVnGy5dqC1c0/b+DoaqbhsPOC45dySmeTs81bjCO99v9ZhY 10 | Oo/gh77bvUTg8c54Jqlr5U8CEMo3gXFVLUetWx/2PhcvA3sWQ07VkglcBkJlXeWO 11 | SrZ7GyZ9De+76LQzo2fA4/P9UitOTq7w8FBmDNwA7afFYoa0ZBs9BNuTem9uzca4 12 | QvqbMavmHQUbvmeb4Sfx+cdKzZt592HdyOSbZyT8W4JGHH1dRWhZstgNc8RktuTN 13 | KVWYOZqRAoGBAM8vbKx3+mIYsgtH7h+zqpLv3a9CYjRgfErf3PMo4NYYPntr0R9Y 14 | l2U7E61G8sjdxW0aMa89HKaMb21bAR/VdzGvdKCZeNBIqXiTb+eBMqt7NkRpOU/B 15 | 2p4HNtMzK/JpdcRxAND61/YTz3J21qELuuwMPCHFz+n5n44bCtZWpIiXAoGBAMyp 16 | IMaO5VuVWnwXWZ+meHaQXZkzHBsGfA9wfe8bOPheONxwfQrnK6Ivtxz7S9nXWixg 17 | Y2qeoYaFg8rmlPbuFXLw4OJhmWEu/qIoW3Y8sHdpeKDiwLrtOu7cP20dO3u8HOFX 18 | eO/YCs+gieUW6p96+ZLqirEFVClPiTQijMJid6NpAoGBALGog/J8Swq1DG8Z9fnf 19 | MWQgJSL0tIsfNVVrEua7ZdiQH3vr3v2XFPMsLlpGXUeay4EblgEjUR8LizzlbVhj 20 | znqfbk2MbImF1TRckPed1NowpD8TT56xpwodO4js90E950tUbxPEFU2gfSE1ACRG 21 | j7l7YFDBc+C5OXU8gRV8ZEfvAoGAQy2g3Iw7LPyxXtorSQRTtldc4dSs/RH71vWN 22 | 4NaGtL/42iLyaInJAMu4x4KVO0Q9DSP2fiDj1EwvHoLhksxDrh7zMlvnBMdwPboR 23 | i5YQNqIgPm8v5CvKlG0nRKG7zLnKoQ0dXV0E73I60T/cc8zh7x+dts2Q+p5o4vwU 24 | SBoaO0ECgYEAzFHnbEVrM7vH0p4lIfx7gc6lG6fJe0hAYeSiFK2wjOQBEcPtptUw 25 | WJUS4MZ6Le3vdgSok3EAINQFC3/Nn+4U/W2ndsdwnOu9KPh7dggTrwysimjnC6AE 26 | lh5fvtxoKdsv5MIceq7N1Os9g0S+8jNROAnQFjvVsysHxzjKpRcqbcU= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/caarlos0/env/v11" 9 | ) 10 | 11 | // config represents the configuration for the Docker client. 12 | // User values are read from the specified environment variables. 13 | type config struct { 14 | // Host is the address of the Docker daemon. 15 | // Default: "" 16 | Host string `env:"DOCKER_HOST"` 17 | 18 | // TLSVerify is a flag to enable or disable TLS verification when connecting to a Docker daemon. 19 | // Default: 0 20 | TLSVerify bool `env:"DOCKER_TLS_VERIFY"` 21 | 22 | // CertPath is the path to the directory containing the Docker certificates. 23 | // This is used when connecting to a Docker daemon over TLS. 24 | // Default: "" 25 | CertPath string `env:"DOCKER_CERT_PATH"` 26 | } 27 | 28 | // newConfig returns a new configuration loaded from the properties file 29 | // located in the user's home directory and overridden by environment variables. 30 | func newConfig(host string) (*config, error) { 31 | cfg := &config{ 32 | Host: host, 33 | } 34 | 35 | if err := env.Parse(cfg); err != nil { 36 | return nil, fmt.Errorf("parse env: %w", err) 37 | } 38 | 39 | if err := cfg.validate(); err != nil { 40 | return nil, fmt.Errorf("validate: %w", err) 41 | } 42 | 43 | return cfg, nil 44 | } 45 | 46 | // validate verifies the configuration is valid. 47 | func (c *config) validate() error { 48 | if c.TLSVerify && c.CertPath == "" { 49 | return errors.New("cert path required when TLS is enabled") 50 | } 51 | 52 | if c.TLSVerify { 53 | if _, err := os.Stat(c.CertPath); os.IsNotExist(err) { 54 | return fmt.Errorf("cert path does not exist: %s", c.CertPath) 55 | } 56 | } 57 | 58 | if c.Host == "" { 59 | return errors.New("host is required") 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /container/wait/exit_test.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | "testing" 9 | "time" 10 | 11 | "github.com/moby/moby/api/types/container" 12 | "github.com/moby/moby/api/types/network" 13 | "github.com/moby/moby/client" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/docker/go-sdk/container/exec" 17 | ) 18 | 19 | type exitStrategyTarget struct { 20 | isRunning bool 21 | } 22 | 23 | func (st *exitStrategyTarget) Host(_ context.Context) (string, error) { 24 | return "", nil 25 | } 26 | 27 | func (st *exitStrategyTarget) Inspect(_ context.Context) (client.ContainerInspectResult, error) { 28 | return client.ContainerInspectResult{}, nil 29 | } 30 | 31 | func (st *exitStrategyTarget) MappedPort(_ context.Context, n network.Port) (network.Port, error) { 32 | return n, nil 33 | } 34 | 35 | func (st *exitStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) { 36 | return nil, nil 37 | } 38 | 39 | func (st *exitStrategyTarget) Exec(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) { 40 | return 0, nil, nil 41 | } 42 | 43 | func (st *exitStrategyTarget) State(_ context.Context) (*container.State, error) { 44 | return &container.State{Running: st.isRunning}, nil 45 | } 46 | 47 | func (st *exitStrategyTarget) CopyFromContainer(context.Context, string) (io.ReadCloser, error) { 48 | return nil, errors.New("not implemented") 49 | } 50 | 51 | func (st *exitStrategyTarget) Logger() *slog.Logger { 52 | return slog.Default() 53 | } 54 | 55 | func TestWaitForExit(t *testing.T) { 56 | target := exitStrategyTarget{ 57 | isRunning: false, 58 | } 59 | wg := NewExitStrategy().WithTimeout(100 * time.Millisecond) 60 | err := wg.WaitUntilReady(context.Background(), &target) 61 | require.NoError(t, err) 62 | } 63 | -------------------------------------------------------------------------------- /network/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/network 2 | 3 | go 1.24.0 4 | 5 | replace ( 6 | github.com/docker/go-sdk/client => ../client 7 | github.com/docker/go-sdk/config => ../config 8 | github.com/docker/go-sdk/context => ../context 9 | ) 10 | 11 | require ( 12 | github.com/containerd/errdefs v1.0.0 13 | github.com/docker/go-sdk/client v0.1.0-alpha011 14 | github.com/google/uuid v1.6.0 15 | github.com/moby/moby/api v1.52.0 16 | github.com/moby/moby/client v0.1.0 17 | github.com/stretchr/testify v1.10.0 18 | ) 19 | 20 | require ( 21 | github.com/Microsoft/go-winio v0.6.2 // indirect 22 | github.com/caarlos0/env/v11 v11.3.1 // indirect 23 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/distribution/reference v0.6.0 // indirect 26 | github.com/docker/go-connections v0.6.0 // indirect 27 | github.com/docker/go-sdk/config v0.1.0-alpha011 // indirect 28 | github.com/docker/go-sdk/context v0.1.0-alpha011 // indirect 29 | github.com/docker/go-units v0.5.0 // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/go-logr/logr v1.4.3 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/moby/docker-image-spec v1.3.1 // indirect 34 | github.com/opencontainers/go-digest v1.0.0 // indirect 35 | github.com/opencontainers/image-spec v1.1.1 // indirect 36 | github.com/pmezard/go-difflib v1.0.0 // indirect 37 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 38 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 39 | go.opentelemetry.io/otel v1.37.0 // indirect 40 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 41 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 42 | golang.org/x/sys v0.33.0 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /volume/volume.find.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "context" 5 | 6 | dockerclient "github.com/moby/moby/client" 7 | 8 | "github.com/docker/go-sdk/client" 9 | ) 10 | 11 | // FindByID finds the volume by ID. 12 | func FindByID(ctx context.Context, volumeID string, opts ...FindOptions) (*Volume, error) { 13 | findOpts := &findOptions{} 14 | for _, opt := range opts { 15 | if err := opt(findOpts); err != nil { 16 | return nil, err 17 | } 18 | } 19 | 20 | if findOpts.client == nil { 21 | sdk, err := client.New(ctx) 22 | if err != nil { 23 | return nil, err 24 | } 25 | findOpts.client = sdk 26 | } 27 | 28 | v, err := findOpts.client.VolumeInspect(ctx, volumeID, dockerclient.VolumeInspectOptions{}) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &Volume{ 34 | Volume: &v.Volume, 35 | dockerClient: findOpts.client, 36 | }, nil 37 | } 38 | 39 | // List lists volumes. 40 | func List(ctx context.Context, opts ...FindOptions) ([]Volume, error) { 41 | findOpts := &findOptions{ 42 | filters: dockerclient.Filters{}, 43 | } 44 | for _, opt := range opts { 45 | if err := opt(findOpts); err != nil { 46 | return nil, err 47 | } 48 | } 49 | 50 | if findOpts.client == nil { 51 | sdk, err := client.New(ctx) 52 | if err != nil { 53 | return nil, err 54 | } 55 | findOpts.client = sdk 56 | } 57 | 58 | response, err := findOpts.client.VolumeList(ctx, dockerclient.VolumeListOptions{ 59 | Filters: findOpts.filters, 60 | }) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | volumes := make([]Volume, len(response.Items)) 66 | for i, v := range response.Items { 67 | volumes[i] = Volume{ 68 | Volume: &v, 69 | dockerClient: findOpts.client, 70 | } 71 | } 72 | 73 | for _, w := range response.Warnings { 74 | findOpts.client.Logger().Warn(w) 75 | } 76 | 77 | return volumes, nil 78 | } 79 | -------------------------------------------------------------------------------- /volume/volume_examples_test.go: -------------------------------------------------------------------------------- 1 | package volume_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/moby/moby/client" 9 | 10 | "github.com/docker/go-sdk/volume" 11 | ) 12 | 13 | func ExampleNew() { 14 | v, err := volume.New(context.Background(), volume.WithName("my-volume")) 15 | if err != nil { 16 | log.Println(err) 17 | return 18 | } 19 | defer func() { 20 | if err := v.Terminate(context.Background()); err != nil { 21 | log.Println(err) 22 | } 23 | }() 24 | 25 | fmt.Println(v.Name) 26 | fmt.Println(v.ID()) 27 | 28 | // Output: 29 | // my-volume 30 | // my-volume 31 | } 32 | 33 | func ExampleFindByID() { 34 | v, err := volume.New(context.Background(), volume.WithName("my-volume-id")) 35 | if err != nil { 36 | log.Println(err) 37 | return 38 | } 39 | defer func() { 40 | if err := v.Terminate(context.Background()); err != nil { 41 | log.Println(err) 42 | } 43 | }() 44 | 45 | vol, err := volume.FindByID(context.Background(), "my-volume-id") 46 | if err != nil { 47 | log.Println(err) 48 | return 49 | } 50 | 51 | fmt.Println(vol.ID()) 52 | fmt.Println(vol.Name) 53 | 54 | // Output: 55 | // my-volume-id 56 | // my-volume-id 57 | } 58 | 59 | func ExampleList() { 60 | v, err := volume.New(context.Background(), volume.WithName("my-volume-list"), volume.WithLabels(map[string]string{"volume.type": "example-test"})) 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | defer func() { 66 | if err := v.Terminate(context.Background()); err != nil { 67 | log.Println(err) 68 | } 69 | }() 70 | 71 | vols, err := volume.List(context.Background(), volume.WithFilters(make(client.Filters).Add("label", "volume.type=example-test"))) 72 | if err != nil { 73 | log.Println(err) 74 | return 75 | } 76 | 77 | fmt.Println(len(vols)) 78 | for _, v := range vols { 79 | fmt.Println(v.ID()) 80 | fmt.Println(v.Name) 81 | } 82 | 83 | // Output: 84 | // 1 85 | // my-volume-list 86 | // my-volume-list 87 | } 88 | -------------------------------------------------------------------------------- /image/build_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "path" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "github.com/docker/go-sdk/client" 15 | "github.com/docker/go-sdk/image" 16 | ) 17 | 18 | var buildPath = path.Join("testdata", "build") 19 | 20 | func BenchmarkBuild(b *testing.B) { 21 | b.Run("success", func(b *testing.B) { 22 | contextArchive, err := image.ArchiveBuildContext(buildPath, "Dockerfile") 23 | require.NoError(b, err) 24 | 25 | // Buffer the entire archive data 26 | archiveData, err := io.ReadAll(contextArchive) 27 | require.NoError(b, err) 28 | 29 | bInfo := &testBuildInfo{ 30 | // using a log writer to avoid writing to stdout, dirtying the benchmark output 31 | logWriter: &bytes.Buffer{}, 32 | } 33 | 34 | b.ResetTimer() 35 | b.ReportAllocs() 36 | 37 | for i := range b.N { 38 | // Create fresh reader from buffered data 39 | bInfo.contextArchive = bytes.NewReader(archiveData) 40 | // Use a unique tag for each iteration to avoid collisions 41 | bInfo.imageTag = fmt.Sprintf("test:benchmark-%d", i) 42 | testBuild(b, bInfo) 43 | } 44 | }) 45 | 46 | b.Run("from-dir", func(b *testing.B) { 47 | buildPath := path.Join("testdata", "build") 48 | 49 | // using a buffer to capture the build output 50 | buf := &bytes.Buffer{} 51 | logger := slog.New(slog.NewTextHandler(buf, nil)) 52 | cli, err := client.New(context.Background(), client.WithLogger(logger)) 53 | require.NoError(b, err) 54 | 55 | b.ResetTimer() 56 | b.ReportAllocs() 57 | 58 | for i := range b.N { 59 | // Use a unique tag for each iteration to avoid collisions 60 | tag := fmt.Sprintf("test:benchmark-%d", i) 61 | _, err := image.BuildFromDir(context.Background(), buildPath, "Dockerfile", tag, image.WithBuildClient(cli)) 62 | require.NoError(b, err) 63 | 64 | b.Cleanup(func() { 65 | cleanup(b, tag) 66 | }) 67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /container/wait/wait.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "time" 10 | 11 | "github.com/moby/moby/api/types/container" 12 | "github.com/moby/moby/api/types/network" 13 | dockerclient "github.com/moby/moby/client" 14 | 15 | "github.com/docker/go-sdk/container/exec" 16 | ) 17 | 18 | // Strategy defines the basic interface for a Wait Strategy 19 | type Strategy interface { 20 | WaitUntilReady(context.Context, StrategyTarget) error 21 | } 22 | 23 | // StrategyTimeout allows MultiStrategy to configure a Strategy's Timeout 24 | type StrategyTimeout interface { 25 | Timeout() *time.Duration 26 | } 27 | 28 | type StrategyTarget interface { 29 | Host(context.Context) (string, error) 30 | Inspect(context.Context) (dockerclient.ContainerInspectResult, error) 31 | MappedPort(context.Context, network.Port) (network.Port, error) 32 | Logs(context.Context) (io.ReadCloser, error) 33 | Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error) 34 | State(context.Context) (*container.State, error) 35 | CopyFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) 36 | Logger() *slog.Logger 37 | } 38 | 39 | func checkTarget(ctx context.Context, target StrategyTarget) error { 40 | state, err := target.State(ctx) 41 | if err != nil { 42 | return fmt.Errorf("get state: %w", err) 43 | } 44 | 45 | return checkState(state) 46 | } 47 | 48 | func checkState(state *container.State) error { 49 | switch { 50 | case state.Running: 51 | return nil 52 | case state.OOMKilled: 53 | return errors.New("container crashed with out-of-memory (OOMKilled)") 54 | case state.Status == "exited": 55 | return fmt.Errorf("container exited with code %d", state.ExitCode) 56 | default: 57 | return fmt.Errorf("unexpected container status %q", state.Status) 58 | } 59 | } 60 | 61 | func defaultTimeout() time.Duration { 62 | return 60 * time.Second 63 | } 64 | 65 | func defaultPollInterval() time.Duration { 66 | return 100 * time.Millisecond 67 | } 68 | -------------------------------------------------------------------------------- /volume/options.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | import ( 4 | "maps" 5 | 6 | dockerclient "github.com/moby/moby/client" 7 | 8 | "github.com/docker/go-sdk/client" 9 | ) 10 | 11 | type options struct { 12 | client client.SDKClient 13 | labels map[string]string 14 | name string 15 | } 16 | 17 | // Option is a function that modifies the options to create a volume. 18 | type Option func(*options) error 19 | 20 | // WithClient sets the docker client. 21 | func WithClient(client client.SDKClient) Option { 22 | return func(o *options) error { 23 | o.client = client 24 | return nil 25 | } 26 | } 27 | 28 | // WithName sets the name of the volume. 29 | func WithName(name string) Option { 30 | return func(o *options) error { 31 | o.name = name 32 | return nil 33 | } 34 | } 35 | 36 | // WithLabels sets the labels of the volume. 37 | func WithLabels(labels map[string]string) Option { 38 | return func(o *options) error { 39 | o.labels = labels 40 | return nil 41 | } 42 | } 43 | 44 | type TerminateOption func(*terminateOptions) error 45 | 46 | type terminateOptions struct { 47 | force bool 48 | } 49 | 50 | // WithForce sets the force option. 51 | func WithForce() TerminateOption { 52 | return func(o *terminateOptions) error { 53 | o.force = true 54 | return nil 55 | } 56 | } 57 | 58 | type findOptions struct { 59 | client client.SDKClient 60 | filters dockerclient.Filters 61 | } 62 | 63 | // FindOptions is a function that modifies the find options 64 | // used to find volumes. 65 | type FindOptions func(opts *findOptions) error 66 | 67 | // WithFindClient returns an [FindOptions] that sets the find client. 68 | func WithFindClient(dockerClient client.SDKClient) FindOptions { 69 | return func(o *findOptions) error { 70 | o.client = dockerClient 71 | return nil 72 | } 73 | } 74 | 75 | // WithFilters sets the filters to be used to filter the volumes. 76 | func WithFilters(filters dockerclient.Filters) FindOptions { 77 | return func(opts *findOptions) error { 78 | opts.filters = maps.Clone(filters) 79 | return nil 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /config/auth/registry.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/distribution/reference" 7 | ) 8 | 9 | const ( 10 | IndexDockerIO = "https://index.docker.io/v1/" 11 | DockerRegistry = "docker.io" 12 | ) 13 | 14 | // ImageReference represents a parsed Docker image reference 15 | type ImageReference struct { 16 | // Registry is the registry hostname (e.g., "docker.io", "myregistry.com:5000") 17 | Registry string 18 | // Repository is the image repository (e.g., "library/nginx", "user/image") 19 | Repository string 20 | // Tag is the image tag (e.g., "latest", "v1.0.0") 21 | Tag string 22 | // Digest is the image digest if present (e.g., "sha256:...") 23 | Digest string 24 | } 25 | 26 | // ParseImageRef extracts the registry from the image name, using github.com/distribution/reference as a reference parser, 27 | // and returns the ImageReference struct. 28 | func ParseImageRef(imageRef string) (ImageReference, error) { 29 | ref, err := reference.ParseAnyReference(imageRef) 30 | if err != nil { 31 | return ImageReference{}, fmt.Errorf("parse image ref: %w", err) 32 | } 33 | 34 | imgRef := ImageReference{} 35 | 36 | named, namedOk := ref.(reference.Named) 37 | if namedOk { 38 | imgRef.Registry = reference.Domain(named) 39 | imgRef.Repository = reference.Path(named) 40 | } 41 | 42 | tagged, ok := ref.(reference.Tagged) 43 | if ok { 44 | imgRef.Tag = tagged.Tag() 45 | } 46 | 47 | digest, ok := ref.(reference.Digested) 48 | if ok { 49 | imgRef.Digest = string(digest.Digest()) 50 | } 51 | 52 | return imgRef, nil 53 | } 54 | 55 | // ResolveRegistryHost can be used to transform a docker registry host name into what is used for the docker config/cred helpers 56 | // 57 | // This is useful for using with containerd authorizers. 58 | // Naturally this only transforms docker hub URLs. 59 | func ResolveRegistryHost(host string) string { 60 | switch host { 61 | case "index.docker.io", "docker.io", IndexDockerIO, "registry-1.docker.io", "index.docker.io/v1", "index.docker.io/v1/": 62 | return IndexDockerIO 63 | } 64 | return host 65 | } 66 | -------------------------------------------------------------------------------- /image/mocks_test.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "iter" 8 | 9 | "github.com/moby/moby/api/types/jsonstream" 10 | "github.com/moby/moby/client" 11 | ) 12 | 13 | // errMockCli is a mock implementation of client.APIClient, which is handy for simulating 14 | // error returns in retry scenarios. 15 | type errMockCli struct { 16 | client.APIClient 17 | 18 | err error 19 | imageBuildCount int 20 | imagePullCount int 21 | } 22 | 23 | func (f *errMockCli) Ping(_ context.Context, _ client.PingOptions) (client.PingResult, error) { 24 | return client.PingResult{}, nil 25 | } 26 | 27 | func (f *errMockCli) ImageBuild(_ context.Context, _ io.Reader, _ client.ImageBuildOptions) (client.ImageBuildResult, error) { 28 | f.imageBuildCount++ 29 | 30 | // In real Docker API, the response body contains JSON build messages, not the build context 31 | // For testing purposes, we can return an empty JSON stream or some mock build output 32 | mockBuildOutput := `{"stream":"Step 1/1 : FROM hello-world"} 33 | {"stream":"Successfully built abc123"} 34 | ` 35 | responseBody := io.NopCloser(bytes.NewBufferString(mockBuildOutput)) 36 | return client.ImageBuildResult{Body: responseBody}, f.err 37 | } 38 | 39 | func (f *errMockCli) ImagePull(_ context.Context, _ string, _ client.ImagePullOptions) (client.ImagePullResponse, error) { 40 | f.imagePullCount++ 41 | // Return mock JSON messages similar to real Docker pull output 42 | mockPullOutput := `{"status":"Pulling from library/nginx","id":"latest"} 43 | {"status":"Pull complete","id":"abc123"} 44 | ` 45 | return errMockImagePullResponse{ReadCloser: io.NopCloser(bytes.NewBufferString(mockPullOutput))}, f.err 46 | } 47 | 48 | func (f *errMockCli) Close() error { 49 | return nil 50 | } 51 | 52 | type errMockImagePullResponse struct { 53 | io.ReadCloser 54 | } 55 | 56 | func (f errMockImagePullResponse) JSONMessages(_ context.Context) iter.Seq2[jsonstream.Message, error] { 57 | return nil 58 | } 59 | 60 | func (f errMockImagePullResponse) Wait(_ context.Context) error { 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/ci-lint-go.yml: -------------------------------------------------------------------------------- 1 | name: Run lint for a Go project 2 | run-name: "${{ inputs.project-directory }}" 3 | 4 | on: 5 | workflow_call: 6 | inputs: 7 | project-directory: 8 | required: true 9 | type: string 10 | default: "." 11 | description: "The directory where the Go project is located." 12 | 13 | permissions: 14 | contents: read 15 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 16 | # pull-requests: read 17 | 18 | jobs: 19 | lint-go-project: 20 | name: "lint: ${{ inputs.project-directory }}" 21 | runs-on: 'ubuntu-latest' 22 | steps: 23 | - name: Check out code into the Go module directory 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | 26 | - name: Set up Go 27 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 28 | with: 29 | go-version-file: "${{ inputs.project-directory == '' && '.' || inputs.project-directory }}/go.mod" 30 | cache-dependency-path: "${{ inputs.project-directory == '' && '.' || inputs.project-directory }}/go.sum" 31 | id: go 32 | 33 | - name: golangci-lint 34 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 35 | with: 36 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 37 | version: v2.0.2 38 | # Optional: working directory, useful for monorepos 39 | working-directory: ${{ inputs.project-directory }} 40 | 41 | - name: generate 42 | working-directory: ./${{ inputs.project-directory }} 43 | shell: bash 44 | run: | 45 | make generate 46 | git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] 47 | 48 | - name: modTidy 49 | working-directory: ./${{ inputs.project-directory }} 50 | shell: bash 51 | run: | 52 | go mod tidy 53 | git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] 54 | -------------------------------------------------------------------------------- /network/options.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/moby/moby/api/types/network" 7 | 8 | "github.com/docker/go-sdk/client" 9 | ) 10 | 11 | type options struct { 12 | client client.SDKClient 13 | ipam *network.IPAM 14 | labels map[string]string 15 | driver string 16 | name string 17 | attachable bool 18 | enableIPv6 bool 19 | internal bool 20 | } 21 | 22 | // Option is a function that modifies the options to create a network. 23 | type Option func(*options) error 24 | 25 | // WithClient sets the docker client. 26 | func WithClient(client client.SDKClient) Option { 27 | return func(o *options) error { 28 | o.client = client 29 | return nil 30 | } 31 | } 32 | 33 | // WithName sets the name of the network. 34 | func WithName(name string) Option { 35 | return func(o *options) error { 36 | if name == "" { 37 | return errors.New("name is required") 38 | } 39 | 40 | o.name = name 41 | return nil 42 | } 43 | } 44 | 45 | // WithDriver sets the driver of the network. 46 | func WithDriver(driver string) Option { 47 | return func(o *options) error { 48 | o.driver = driver 49 | return nil 50 | } 51 | } 52 | 53 | // WithInternal makes the network internal. 54 | func WithInternal() Option { 55 | return func(o *options) error { 56 | o.internal = true 57 | return nil 58 | } 59 | } 60 | 61 | // WithEnableIPv6 enables IPv6 on the network. 62 | func WithEnableIPv6() Option { 63 | return func(o *options) error { 64 | o.enableIPv6 = true 65 | return nil 66 | } 67 | } 68 | 69 | // WithAttachable makes the network attachable. 70 | func WithAttachable() Option { 71 | return func(o *options) error { 72 | o.attachable = true 73 | return nil 74 | } 75 | } 76 | 77 | // WithLabels sets the labels of the network. 78 | func WithLabels(labels map[string]string) Option { 79 | return func(o *options) error { 80 | o.labels = labels 81 | return nil 82 | } 83 | } 84 | 85 | // WithIPAM sets the IPAM of the network. 86 | func WithIPAM(ipam *network.IPAM) Option { 87 | return func(o *options) error { 88 | o.ipam = ipam 89 | return nil 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /network/README.md: -------------------------------------------------------------------------------- 1 | # Docker Networks 2 | 3 | This package provides a simple API to create and manage Docker networks. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/docker/go-sdk/network 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```go 14 | nw, err := network.New(ctx) 15 | if err != nil { 16 | log.Fatalf("failed to create network: %v", err) 17 | } 18 | 19 | resp, err := nw.Inspect(ctx) 20 | if err != nil { 21 | log.Fatalf("failed to inspect network: %v", err) 22 | } 23 | 24 | fmt.Printf("network: %+v", resp) 25 | 26 | inspect, err := network.FindByID(ctx, nw.ID()) 27 | if err != nil { 28 | log.Fatalf("failed to find network by id: %v", err) 29 | } 30 | 31 | inspect, err = network.FindByName(ctx, nw.Name()) 32 | if err != nil { 33 | log.Fatalf("failed to find network by name: %v", err) 34 | } 35 | 36 | _, err = network.List(ctx) 37 | if err != nil { 38 | log.Fatalf("failed to list networks: %v", err) 39 | } 40 | 41 | _, err = network.List(ctx, network.WithFilters(filters.NewArgs(filters.Arg("driver", "bridge")))) 42 | if err != nil { 43 | log.Fatalf("failed to list networks with filters: %v", err) 44 | } 45 | 46 | err = nw.Terminate(ctx) 47 | if err != nil { 48 | log.Fatalf("failed to terminate network: %v", err) 49 | } 50 | ``` 51 | 52 | ## Customizing the network 53 | 54 | The network created with the `New` function can be customized using functional options. The following options are available: 55 | 56 | - `WithClient(client client.SDKClient) network.Option`: The client to use to create the network. If not provided, the default client will be used. 57 | - `WithName(name string) network.Option`: The name of the network. 58 | - `WithDriver(driver string) network.Option`: The driver of the network. 59 | - `WithInternal() network.Option`: Whether the network is internal. 60 | - `WithEnableIPv6() network.Option`: Whether the network is IPv6 enabled. 61 | - `WithAttachable() network.Option`: Whether the network is attachable. 62 | - `WithLabels(labels map[string]string) network.Option`: The labels of the network. 63 | - `WithIPAM(ipam *network.IPAM) network.Option`: The IPAM configuration of the network. 64 | -------------------------------------------------------------------------------- /context/context.add.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/opencontainers/go-digest" 8 | 9 | "github.com/docker/go-sdk/config" 10 | ) 11 | 12 | // New creates a new context. 13 | // 14 | // If the context already exists, it returns an error. 15 | // 16 | // If the [AsCurrent] option is passed, it updates the Docker config 17 | // file, setting the current context to the new context. 18 | func New(name string, opts ...CreateContextOption) (*Context, error) { 19 | switch name { 20 | case "": 21 | return nil, errors.New("name is required") 22 | case "default": 23 | return nil, errors.New("name cannot be 'default'") 24 | } 25 | 26 | _, err := Inspect(name) 27 | if err == nil { 28 | return nil, fmt.Errorf("context %s already exists", name) 29 | } 30 | 31 | defaultOptions := &contextOptions{} 32 | for _, opt := range opts { 33 | if err := opt(defaultOptions); err != nil { 34 | return nil, fmt.Errorf("apply option: %w", err) 35 | } 36 | } 37 | 38 | ctx := &Context{ 39 | Name: name, 40 | encodedName: digest.FromString(name).Encoded(), 41 | Metadata: &Metadata{ 42 | Description: defaultOptions.description, 43 | additionalFields: defaultOptions.additionalFields, 44 | }, 45 | Endpoints: map[string]*endpoint{ 46 | "docker": { 47 | Host: defaultOptions.host, 48 | SkipTLSVerify: defaultOptions.skipTLSVerify, 49 | }, 50 | }, 51 | } 52 | 53 | metaRoot, err := metaRoot() 54 | if err != nil { 55 | return nil, fmt.Errorf("meta root: %w", err) 56 | } 57 | 58 | s := &store{root: metaRoot} 59 | 60 | if err := s.add(ctx); err != nil { 61 | return nil, fmt.Errorf("add context: %w", err) 62 | } 63 | 64 | // set the context as the current context if the option is set 65 | if defaultOptions.current { 66 | cfg, err := config.Load() 67 | if err != nil { 68 | return nil, fmt.Errorf("load config: %w", err) 69 | } 70 | 71 | cfg.CurrentContext = ctx.Name 72 | 73 | if err := cfg.Save(); err != nil { 74 | return nil, fmt.Errorf("save config: %w", err) 75 | } 76 | 77 | ctx.isCurrent = true 78 | } 79 | 80 | return ctx, nil 81 | } 82 | -------------------------------------------------------------------------------- /container/container.network.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "net/netip" 6 | ) 7 | 8 | // ContainerIP gets the IP address of the primary network within the container. 9 | // If there are multiple networks, it returns an empty string. 10 | func (c *Container) ContainerIP(ctx context.Context) (netip.Addr, error) { 11 | inspect, err := c.Inspect(ctx) 12 | if err != nil { 13 | return netip.Addr{}, err 14 | } 15 | 16 | // use IP from "Networks" if only single network defined 17 | var ip netip.Addr 18 | networks := inspect.Container.NetworkSettings.Networks 19 | if len(networks) == 1 { 20 | for _, v := range networks { 21 | ip = v.IPAddress 22 | } 23 | } 24 | return ip, nil 25 | } 26 | 27 | // ContainerIPs gets the IP addresses of all the networks within the container. 28 | func (c *Container) ContainerIPs(ctx context.Context) ([]netip.Addr, error) { 29 | inspect, err := c.Inspect(ctx) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | ips := make([]netip.Addr, 0, len(inspect.Container.NetworkSettings.Networks)) 35 | for _, nw := range inspect.Container.NetworkSettings.Networks { 36 | ips = append(ips, nw.IPAddress) 37 | } 38 | 39 | return ips, nil 40 | } 41 | 42 | // NetworkAliases gets the aliases of the container for the networks it is attached to. 43 | func (c *Container) NetworkAliases(ctx context.Context) (map[string][]string, error) { 44 | inspect, err := c.Inspect(ctx) 45 | if err != nil { 46 | return map[string][]string{}, err 47 | } 48 | 49 | networks := inspect.Container.NetworkSettings.Networks 50 | 51 | a := map[string][]string{} 52 | 53 | for k := range networks { 54 | a[k] = networks[k].Aliases 55 | } 56 | 57 | return a, nil 58 | } 59 | 60 | // Networks gets the names of the networks the container is attached to. 61 | func (c *Container) Networks(ctx context.Context) ([]string, error) { 62 | inspect, err := c.Inspect(ctx) 63 | if err != nil { 64 | return []string{}, err 65 | } 66 | 67 | networks := inspect.Container.NetworkSettings.Networks 68 | 69 | var n []string 70 | 71 | for k := range networks { 72 | n = append(n, k) 73 | } 74 | 75 | return n, nil 76 | } 77 | -------------------------------------------------------------------------------- /container/wait/walk.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrVisitStop is used as a return value from [VisitFunc] to stop the walk. 9 | // It is not returned as an error by any function. 10 | ErrVisitStop = errors.New("stop the walk") 11 | 12 | // ErrVisitRemove is used as a return value from [VisitFunc] to have the current node removed. 13 | // It is not returned as an error by any function. 14 | ErrVisitRemove = errors.New("remove this strategy") 15 | ) 16 | 17 | // VisitFunc is a function that visits a strategy node. 18 | // If it returns [ErrVisitStop], the walk stops. 19 | // If it returns [ErrVisitRemove], the current node is removed. 20 | type VisitFunc func(root Strategy) error 21 | 22 | // Walk walks the strategies tree and calls the visit function for each node. 23 | func Walk(root *Strategy, visit VisitFunc) error { 24 | if root == nil { 25 | return errors.New("root strategy is nil") 26 | } 27 | 28 | if err := walk(root, visit); err != nil { 29 | if errors.Is(err, ErrVisitRemove) || errors.Is(err, ErrVisitStop) { 30 | return nil 31 | } 32 | return err 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // walk walks the strategies tree and calls the visit function for each node. 39 | // It returns an error if the visit function returns an error. 40 | func walk(root *Strategy, visit VisitFunc) error { 41 | if *root == nil { 42 | // No strategy. 43 | return nil 44 | } 45 | 46 | // Allow the visit function to customize the behaviour of the walk before visiting the children. 47 | if err := visit(*root); err != nil { 48 | if errors.Is(err, ErrVisitRemove) { 49 | *root = nil 50 | } 51 | 52 | return err 53 | } 54 | 55 | if s, ok := (*root).(*MultiStrategy); ok { 56 | var i int 57 | for range s.Strategies { 58 | if err := walk(&s.Strategies[i], visit); err != nil { 59 | if errors.Is(err, ErrVisitRemove) { 60 | s.Strategies = append(s.Strategies[:i], s.Strategies[i+1:]...) 61 | if errors.Is(err, ErrVisitStop) { 62 | return ErrVisitStop 63 | } 64 | continue 65 | } 66 | 67 | return err 68 | } 69 | i++ 70 | } 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/conventions.yml: -------------------------------------------------------------------------------- 1 | name: "Enforce conventions" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - reopened 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | lint-pr: 16 | name: Validate PR title follows Conventional Commits 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | # We may not need a scope on every commit (i.e. repo-level change). 24 | # 25 | # feat!: read config consistently 26 | # feat(config): support for A 27 | # chore(config): update tests 28 | # fix(config): trim value 29 | # ^ ^ ^ 30 | # | | |__ Subject 31 | # | |_______ Scope 32 | # |____________ Type: it can end with a ! to denote a breaking change. 33 | requireScope: false 34 | # Scope should be lowercase. 35 | disallowScopes: | 36 | [A-Z]+ 37 | # ensures the subject doesn't start with an uppercase character. 38 | subjectPattern: ^(?![A-Z]).+$ 39 | subjectPatternError: | 40 | The subject "{subject}" found in the pull request title "{title}" 41 | didn't match the configured pattern. Please ensure that the subject 42 | doesn't start with an uppercase character. 43 | types: | 44 | security 45 | fix 46 | feat 47 | docs 48 | chore 49 | deps 50 | 51 | - name: Detect if the Pull Request was sent from the main branch 52 | if: "${{ github.head_ref == 'main' }}" 53 | env: 54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: | 56 | gh pr close --comment "This Pull Request has been automatically closed because it was sent from the fork's main branch. Please use a different branch so that the maintainers can contribute to your Pull Request." 57 | exit 1 58 | -------------------------------------------------------------------------------- /network/network.inspect.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/moby/moby/client" 8 | ) 9 | 10 | type inspectOptions struct { 11 | cache bool 12 | options client.NetworkInspectOptions 13 | } 14 | 15 | // InspectOptions is a function that modifies the inspect options. 16 | type InspectOptions func(opts *inspectOptions) error 17 | 18 | // WithNoCache returns an InspectOptions that disables caching the result of the inspection. 19 | // If passed, the Docker daemon will be queried for the latest information, so it can be 20 | // used for refreshing the cached result of a previous inspection. 21 | func WithNoCache() InspectOptions { 22 | return func(o *inspectOptions) error { 23 | o.cache = false 24 | return nil 25 | } 26 | } 27 | 28 | // WithInspectOptions returns an InspectOptions that sets the inspect options. 29 | func WithInspectOptions(opts client.NetworkInspectOptions) InspectOptions { 30 | return func(o *inspectOptions) error { 31 | o.options = opts 32 | return nil 33 | } 34 | } 35 | 36 | // Inspect inspects the network, caching the results. 37 | func (n *Network) Inspect(ctx context.Context, opts ...InspectOptions) (client.NetworkInspectResult, error) { 38 | if n.dockerClient == nil { 39 | return client.NetworkInspectResult{}, errors.New("docker client is not initialized") 40 | } 41 | 42 | inspectOptions := &inspectOptions{ 43 | cache: true, // cache the result by default 44 | } 45 | for _, opt := range opts { 46 | if err := opt(inspectOptions); err != nil { 47 | return client.NetworkInspectResult{}, err 48 | } 49 | } 50 | 51 | if inspectOptions.cache { 52 | // if the result was already cached, return it 53 | if n.inspect.Network.ID != "" { 54 | return n.inspect, nil 55 | } 56 | 57 | // else, log a warning and inspect the network 58 | n.dockerClient.Logger().Warn("network not inspected yet, inspecting now", "network", n.ID(), "cache", inspectOptions.cache) 59 | } 60 | 61 | inspect, err := n.dockerClient.NetworkInspect(ctx, n.ID(), inspectOptions.options) 62 | if err != nil { 63 | return client.NetworkInspectResult{}, err 64 | } 65 | 66 | // cache the result for subsequent calls 67 | n.inspect = inspect 68 | 69 | return inspect, nil 70 | } 71 | -------------------------------------------------------------------------------- /container/wait/wait_test.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log/slog" 8 | 9 | "github.com/moby/moby/api/types/container" 10 | "github.com/moby/moby/api/types/network" 11 | "github.com/moby/moby/client" 12 | 13 | "github.com/docker/go-sdk/container/exec" 14 | ) 15 | 16 | var ErrPortNotFound = errors.New("port not found") 17 | 18 | type MockStrategyTarget struct { 19 | HostImpl func(context.Context) (string, error) 20 | InspectImpl func(context.Context) (client.ContainerInspectResult, error) 21 | PortsImpl func(context.Context) (network.PortMap, error) 22 | MappedPortImpl func(context.Context, network.Port) (network.Port, error) 23 | LogsImpl func(context.Context) (io.ReadCloser, error) 24 | ExecImpl func(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error) 25 | StateImpl func(context.Context) (*container.State, error) 26 | CopyFromContainerImpl func(context.Context, string) (io.ReadCloser, error) 27 | LoggerImpl func() *slog.Logger 28 | } 29 | 30 | func (st *MockStrategyTarget) Host(ctx context.Context) (string, error) { 31 | return st.HostImpl(ctx) 32 | } 33 | 34 | func (st *MockStrategyTarget) Inspect(ctx context.Context) (client.ContainerInspectResult, error) { 35 | return st.InspectImpl(ctx) 36 | } 37 | 38 | func (st *MockStrategyTarget) MappedPort(ctx context.Context, port network.Port) (network.Port, error) { 39 | return st.MappedPortImpl(ctx, port) 40 | } 41 | 42 | func (st *MockStrategyTarget) Logs(ctx context.Context) (io.ReadCloser, error) { 43 | return st.LogsImpl(ctx) 44 | } 45 | 46 | func (st *MockStrategyTarget) Exec(ctx context.Context, cmd []string, options ...exec.ProcessOption) (int, io.Reader, error) { 47 | return st.ExecImpl(ctx, cmd, options...) 48 | } 49 | 50 | func (st *MockStrategyTarget) State(ctx context.Context) (*container.State, error) { 51 | return st.StateImpl(ctx) 52 | } 53 | 54 | func (st *MockStrategyTarget) CopyFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) { 55 | return st.CopyFromContainerImpl(ctx, filePath) 56 | } 57 | 58 | func (st *MockStrategyTarget) Logger() *slog.Logger { 59 | return st.LoggerImpl() 60 | } 61 | -------------------------------------------------------------------------------- /container/container.exec.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "github.com/moby/moby/client" 10 | 11 | "github.com/docker/go-sdk/container/exec" 12 | ) 13 | 14 | // Exec executes a command in the current container. 15 | // It returns the exit status of the executed command, an [io.Reader] containing the combined 16 | // stdout and stderr, and any encountered error. Note that reading directly from the [io.Reader] 17 | // may result in unexpected bytes due to custom stream multiplexing headers. 18 | // Use [cexec.Multiplexed] option to read the combined output without the multiplexing headers. 19 | // Alternatively, to separate the stdout and stderr from [io.Reader] and interpret these headers properly, 20 | // [github.com/moby/moby/api/pkg/stdcopy.StdCopy] from the Docker API should be used. 21 | func (c *Container) Exec(ctx context.Context, cmd []string, options ...exec.ProcessOption) (int, io.Reader, error) { 22 | processOptions := exec.NewProcessOptions(cmd) 23 | 24 | // processing all the options in a first loop because for the multiplexed option 25 | // we first need to have a containerExecCreateResponse 26 | for _, o := range options { 27 | o.Apply(processOptions) 28 | } 29 | 30 | response, err := c.dockerClient.ExecCreate(ctx, c.ID(), processOptions.ExecConfig) 31 | if err != nil { 32 | return 0, nil, fmt.Errorf("container exec create: %w", err) 33 | } 34 | 35 | hijack, err := c.dockerClient.ExecAttach(ctx, response.ID, client.ExecAttachOptions{}) 36 | if err != nil { 37 | return 0, nil, fmt.Errorf("container exec attach: %w", err) 38 | } 39 | 40 | processOptions.Reader = hijack.Reader 41 | 42 | // second loop to process the multiplexed option, as now we have a reader 43 | // from the created exec response. 44 | for _, o := range options { 45 | o.Apply(processOptions) 46 | } 47 | 48 | var exitCode int 49 | for { 50 | execResp, err := c.dockerClient.ExecInspect(ctx, response.ID, client.ExecInspectOptions{}) 51 | if err != nil { 52 | return 0, nil, fmt.Errorf("container exec inspect: %w", err) 53 | } 54 | 55 | if !execResp.Running { 56 | exitCode = execResp.ExitCode 57 | break 58 | } 59 | 60 | time.Sleep(100 * time.Millisecond) 61 | } 62 | 63 | return exitCode, processOptions.Reader, nil 64 | } 65 | -------------------------------------------------------------------------------- /image/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/image 2 | 3 | go 1.24.0 4 | 5 | replace ( 6 | github.com/docker/go-sdk/client => ../client 7 | github.com/docker/go-sdk/config => ../config 8 | github.com/docker/go-sdk/context => ../context 9 | ) 10 | 11 | require ( 12 | github.com/cenkalti/backoff/v4 v4.2.1 13 | github.com/containerd/errdefs v1.0.0 14 | github.com/docker/go-sdk/client v0.1.0-alpha011 15 | github.com/docker/go-sdk/config v0.1.0-alpha011 16 | github.com/moby/go-archive v0.1.0 17 | github.com/moby/moby/api v1.52.0 18 | github.com/moby/moby/client v0.1.0 19 | github.com/moby/patternmatcher v0.6.0 20 | github.com/moby/term v0.5.2 21 | github.com/opencontainers/image-spec v1.1.1 22 | github.com/stretchr/testify v1.10.0 23 | ) 24 | 25 | require ( 26 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 27 | github.com/Microsoft/go-winio v0.6.2 // indirect 28 | github.com/caarlos0/env/v11 v11.3.1 // indirect 29 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 30 | github.com/containerd/log v0.1.0 // indirect 31 | github.com/davecgh/go-spew v1.1.1 // indirect 32 | github.com/distribution/reference v0.6.0 // indirect 33 | github.com/docker/go-connections v0.6.0 // indirect 34 | github.com/docker/go-sdk/context v0.1.0-alpha011 // indirect 35 | github.com/docker/go-units v0.5.0 // indirect 36 | github.com/felixge/httpsnoop v1.0.4 // indirect 37 | github.com/go-logr/logr v1.4.3 // indirect 38 | github.com/go-logr/stdr v1.2.2 // indirect 39 | github.com/klauspost/compress v1.18.0 // indirect 40 | github.com/moby/docker-image-spec v1.3.1 // indirect 41 | github.com/moby/sys/sequential v0.6.0 // indirect 42 | github.com/moby/sys/user v0.4.0 // indirect 43 | github.com/moby/sys/userns v0.1.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/sirupsen/logrus v1.9.3 // indirect 47 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 48 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 49 | go.opentelemetry.io/otel v1.37.0 // indirect 50 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 51 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 52 | golang.org/x/sys v0.33.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Docker Config 2 | 3 | This package provides a simple API to load docker CLI configs, auths, etc. with minimal deps. 4 | 5 | This library is a fork of [github.com/cpuguy83/dockercfg](https://github.com/cpuguy83/dockercfg). Read the [NOTICE](../NOTICE) file for more details. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | go get github.com/docker/go-sdk/config 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Docker Config 16 | 17 | #### Directory 18 | 19 | It returns the current Docker config directory. 20 | 21 | ```go 22 | dir, err := config.Dir() 23 | if err != nil { 24 | log.Fatalf("failed to get current docker config directory: %v", err) 25 | } 26 | 27 | fmt.Printf("current docker config directory: %s", dir) 28 | ``` 29 | 30 | #### Filepath 31 | 32 | It returns the path to the Docker config file. 33 | 34 | ```go 35 | filepath, err := config.Filepath() 36 | if err != nil { 37 | log.Fatalf("failed to get current docker config file path: %v", err) 38 | } 39 | 40 | fmt.Printf("current docker config file path: %s", filepath) 41 | ``` 42 | 43 | #### Load 44 | 45 | It returns the Docker config. 46 | 47 | ```go 48 | cfg, err := config.Load() 49 | if err != nil { 50 | log.Fatalf("failed to load docker config: %v", err) 51 | } 52 | 53 | fmt.Printf("docker config: %+v", cfg) 54 | ``` 55 | 56 | #### Save 57 | 58 | Once you have loaded a config, you can save it back to the file system. 59 | 60 | ```go 61 | if err := cfg.Save(); err != nil { 62 | log.Fatalf("failed to save docker config: %v", err) 63 | } 64 | ``` 65 | 66 | ### Auth 67 | 68 | #### AuthConfigs 69 | 70 | It returns a maps of the registry credentials for the given Docker images, indexed by the registry hostname. 71 | 72 | ```go 73 | authConfigs, err := config.AuthConfigs("nginx:latest") 74 | if err != nil { 75 | log.Fatalf("failed to get registry credentials: %v", err) 76 | } 77 | 78 | fmt.Printf("registry credentials: %+v", authConfigs) 79 | ``` 80 | 81 | #### Auth Configs For Hostname 82 | 83 | It returns the registry credentials for the given Docker registry. 84 | 85 | ```go 86 | authConfig, err := config.AuthConfigForHostname("https://index.docker.io/v1/") 87 | if err != nil { 88 | log.Fatalf("failed to get registry credentials: %v", err) 89 | } 90 | 91 | fmt.Printf("registry credentials: %+v", authConfig) 92 | ``` 93 | -------------------------------------------------------------------------------- /client/daemon.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/netip" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/moby/moby/client" 11 | ) 12 | 13 | // dockerEnvFile is the file that is created when running inside a container. 14 | // It's a variable to allow testing. 15 | var dockerEnvFile = "/.dockerenv" 16 | 17 | // DaemonHostWithContext gets the host or ip of the Docker daemon where ports are exposed on 18 | // Warning: this is based on your Docker host setting. Will fail if using an SSH tunnel 19 | func (c *sdkClient) DaemonHostWithContext(ctx context.Context) (string, error) { 20 | c.mtx.Lock() 21 | defer c.mtx.Unlock() 22 | 23 | return c.daemonHostLocked(ctx) 24 | } 25 | 26 | func (c *sdkClient) daemonHostLocked(ctx context.Context) (string, error) { 27 | // infer from Docker host 28 | daemonURL, err := url.Parse(c.DaemonHost()) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | var host string 34 | 35 | switch daemonURL.Scheme { 36 | case "http", "https", "tcp": 37 | host = daemonURL.Hostname() 38 | case "unix", "npipe": 39 | if inAContainer(dockerEnvFile) { 40 | ip, err := c.getGatewayIP(ctx, "bridge") 41 | if err != nil { 42 | host = "localhost" 43 | } else { 44 | host = ip.String() 45 | } 46 | } else { 47 | host = "localhost" 48 | } 49 | default: 50 | return "", errors.New("could not determine host through env or docker host") 51 | } 52 | 53 | return host, nil 54 | } 55 | 56 | func (c *sdkClient) getGatewayIP(ctx context.Context, defaultNetwork string) (netip.Addr, error) { 57 | nw, err := c.NetworkInspect(ctx, defaultNetwork, client.NetworkInspectOptions{}) 58 | if err != nil { 59 | return netip.Addr{}, err 60 | } 61 | 62 | var ip netip.Addr 63 | for _, cfg := range nw.Network.IPAM.Config { 64 | if cfg.Gateway.IsValid() { 65 | ip = cfg.Gateway 66 | break 67 | } 68 | } 69 | if !ip.IsValid() { 70 | return netip.Addr{}, errors.New("failed to get gateway IP from network settings") 71 | } 72 | 73 | return ip, nil 74 | } 75 | 76 | // InAContainer returns true if the code is running inside a container 77 | // See https://github.com/docker/docker/blob/a9fa38b1edf30b23cae3eade0be48b3d4b1de14b/daemon/initlayer/setup_unix.go#L25 78 | func inAContainer(path string) bool { 79 | if _, err := os.Stat(path); err == nil { 80 | return true 81 | } 82 | return false 83 | } 84 | -------------------------------------------------------------------------------- /volume/README.md: -------------------------------------------------------------------------------- 1 | # Docker Volumes 2 | 3 | This package provides a simple API to create and manage Docker volumes. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/docker/go-sdk/volume 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```go 14 | v, err := volume.New(context.Background(), volume.WithName("my-volume-list"), volume.WithLabels(map[string]string{"volume.type": "example-test"})) 15 | if err != nil { 16 | log.Println(err) 17 | return 18 | } 19 | defer func() { 20 | if err := v.Terminate(context.Background()); err != nil { 21 | log.Println(err) 22 | } 23 | }() 24 | fmt.Printf("volume: %+v", vol) 25 | 26 | vol, err := volume.FindByID(context.Background(), v.ID()) 27 | if err != nil { 28 | log.Println(err) 29 | return 30 | } 31 | fmt.Printf("volume: %+v", vol) 32 | 33 | vols, err := volume.List(context.Background(), make(client.Filters).Add("label", "volume.type=example-test")) 34 | if err != nil { 35 | log.Println(err) 36 | return 37 | } 38 | 39 | fmt.Println(len(vols)) 40 | for _, v := range vols { 41 | fmt.Printf("%s", v.Name) 42 | } 43 | 44 | err = v.Terminate(ctx) 45 | if err != nil { 46 | log.Fatalf("failed to terminate volume: %v", err) 47 | } 48 | ``` 49 | 50 | ## Customizing the volume 51 | 52 | The volume created with the `New` function can be customized using functional options. The following options are available: 53 | 54 | - `WithClient(client client.SDKClient) volume.Option`: The client to use to create the volume. If not provided, the default client will be used. 55 | - `WithName(name string) volume.Option`: The name of the volume. 56 | - `WithLabels(labels map[string]string) volume.Option`: The labels of the volume. 57 | 58 | When terminating a volume, the `Terminate` function can be customized using functional options. The following options are available: 59 | 60 | - `WithForce() volume.TerminateOption`: Whether to force the termination of the volume. 61 | 62 | When finding a volume, the `FindByID` and `List` functions can be customized using functional options. The following options are available: 63 | 64 | - `WithFindClient(client *client.Client) volume.FindOptions`: The client to use to find the volume. If not provided, the default client will be used. 65 | - `WithFilters(filters client.Filters) volume.FindOptions`: The filters to use to find the volume. In the case of the `FindByID` function, this option is ignored. 66 | -------------------------------------------------------------------------------- /.github/scripts/refresh-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ============================================================================= 4 | # Go Proxy Refresh Script 5 | # ============================================================================= 6 | # Description: Triggers the Go proxy to refresh/fetch a module version 7 | # This is useful to ensure pkg.go.dev has the latest version 8 | # 9 | # Usage: ./.github/scripts/refresh-proxy.sh 10 | # 11 | # Arguments: 12 | # module - Name of the module to refresh (required) 13 | # Examples: client, container, config, context, image, network 14 | # 15 | # Examples: 16 | # ./.github/scripts/refresh-proxy.sh client 17 | # ./.github/scripts/refresh-proxy.sh container 18 | # 19 | # Dependencies: 20 | # - git (for finding latest tag) 21 | # - curl (for triggering Go proxy) 22 | # 23 | # ============================================================================= 24 | 25 | set -e 26 | 27 | # Source common functions 28 | readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 29 | source "${SCRIPT_DIR}/common.sh" 30 | 31 | # Get module name from argument and lowercase it 32 | readonly MODULE=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') 33 | 34 | if [[ -z "$MODULE" ]]; then 35 | echo "Error: Module name is required" 36 | echo "Usage: $0 " 37 | echo "Example: $0 client" 38 | exit 1 39 | fi 40 | 41 | echo "Refreshing Go proxy for module: ${MODULE}" 42 | 43 | # Check if version.go exists 44 | readonly VERSION_FILE="${ROOT_DIR}/${MODULE}/version.go" 45 | if [[ ! -f "${VERSION_FILE}" ]]; then 46 | echo "Error: version.go not found at ${VERSION_FILE}" 47 | exit 1 48 | fi 49 | 50 | # Read version from version.go 51 | VERSION=$(get_version_from_file "${VERSION_FILE}") 52 | 53 | if [[ -z "$VERSION" ]]; then 54 | echo "Error: Could not extract version from ${VERSION_FILE}" 55 | exit 1 56 | fi 57 | 58 | # Ensure version has v prefix for the tag 59 | if [[ ! "${VERSION}" =~ ^v ]]; then 60 | VERSION="v${VERSION}" 61 | fi 62 | 63 | echo "Current version: ${VERSION}" 64 | echo "Triggering Go proxy refresh..." 65 | 66 | # Trigger Go proxy (bypass dry-run since this is a read-only operation) 67 | DRY_RUN=false curlGolangProxy "${MODULE}" "${VERSION}" 68 | 69 | echo "✅ Go proxy refresh completed for ${MODULE}@${VERSION}" 70 | echo "The module should be available at: https://pkg.go.dev/${GITHUB_REPO}/${MODULE}@${VERSION}" 71 | -------------------------------------------------------------------------------- /network/network.list_test.go: -------------------------------------------------------------------------------- 1 | package network_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | dockerclient "github.com/moby/moby/client" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/client" 11 | "github.com/docker/go-sdk/network" 12 | ) 13 | 14 | func TestFindByID(t *testing.T) { 15 | nw, err := network.New(context.Background(), network.WithName("test-by-id")) 16 | network.Cleanup(t, nw) 17 | require.NoError(t, err) 18 | 19 | inspect, err := network.FindByID(context.Background(), nw.ID()) 20 | require.NoError(t, err) 21 | require.Equal(t, nw.ID(), inspect.ID) 22 | 23 | no, err := network.FindByID(context.Background(), "not-found-id") 24 | require.Error(t, err) 25 | require.Empty(t, no.ID) 26 | } 27 | 28 | func TestFindByName(t *testing.T) { 29 | nw, err := network.New(context.Background(), network.WithName("test-by-name")) 30 | network.Cleanup(t, nw) 31 | require.NoError(t, err) 32 | 33 | inspect, err := network.FindByName(context.Background(), nw.Name()) 34 | require.NoError(t, err) 35 | require.Equal(t, nw.Name(), inspect.Name) 36 | 37 | no, err := network.FindByName(context.Background(), "not-found-name") 38 | require.Error(t, err) 39 | require.Empty(t, no.Name) 40 | } 41 | 42 | func TestList(t *testing.T) { 43 | nws, err := network.List(context.Background()) 44 | require.NoError(t, err) 45 | initialCount := len(nws) 46 | 47 | t.Run("no-filters", func(t *testing.T) { 48 | max := 5 49 | for range max { 50 | nw, err := network.New(context.Background()) 51 | network.Cleanup(t, nw) 52 | require.NoError(t, err) 53 | } 54 | 55 | nws, err = network.List(context.Background()) 56 | require.NoError(t, err) 57 | require.Len(t, nws, initialCount+max) 58 | }) 59 | 60 | t.Run("with-filters", func(t *testing.T) { 61 | nws, err = network.List(context.Background(), 62 | network.WithFilters(make(dockerclient.Filters).Add("driver", "bridge")), 63 | ) 64 | require.NoError(t, err) 65 | require.Len(t, nws, 1) 66 | }) 67 | 68 | t.Run("with-list-client", func(t *testing.T) { 69 | dockerClient, err := client.New(context.Background()) 70 | require.NoError(t, err) 71 | 72 | nw, err := network.New(context.Background(), network.WithClient(dockerClient)) 73 | network.Cleanup(t, nw) 74 | require.NoError(t, err) 75 | 76 | nws, err = network.List(context.Background(), network.WithListClient(dockerClient)) 77 | require.NoError(t, err) 78 | require.Len(t, nws, initialCount+1) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | output: 3 | formats: 4 | text: 5 | path: stdout 6 | path-prefix: . 7 | linters: 8 | enable: 9 | - errorlint 10 | - gocritic 11 | - misspell 12 | - nakedret 13 | - nolintlint 14 | - perfsprint 15 | - revive 16 | - testifylint 17 | - thelper 18 | - usestdlibvars 19 | settings: 20 | errorlint: 21 | errorf: true 22 | errorf-multi: true 23 | asserts: true 24 | comparison: true 25 | revive: 26 | rules: 27 | - name: blank-imports 28 | - name: context-as-argument 29 | arguments: 30 | - allowTypesBefore: '*testing.T,*testing.B' 31 | - name: context-keys-type 32 | - name: dot-imports 33 | - name: early-return 34 | arguments: 35 | - preserveScope 36 | - name: empty-block 37 | - name: error-naming 38 | disabled: true 39 | - name: error-return 40 | - name: error-strings 41 | disabled: true 42 | - name: errorf 43 | - name: increment-decrement 44 | - name: indent-error-flow 45 | arguments: 46 | - preserveScope 47 | - name: range 48 | - name: receiver-naming 49 | - name: redefines-builtin-id 50 | disabled: true 51 | - name: superfluous-else 52 | arguments: 53 | - preserveScope 54 | - name: time-naming 55 | - name: unexported-return 56 | disabled: true 57 | - name: unreachable-code 58 | - name: unused-parameter 59 | - name: use-any 60 | - name: var-declaration 61 | - name: var-naming 62 | arguments: 63 | - - ID 64 | - - VM 65 | - - upperCaseConst: true 66 | testifylint: 67 | enable-all: true 68 | disable: 69 | - float-compare 70 | - go-require 71 | exclusions: 72 | generated: lax 73 | presets: 74 | - comments 75 | - common-false-positives 76 | - legacy 77 | - std-error-handling 78 | paths: 79 | - third_party$ 80 | - builtin$ 81 | - examples$ 82 | formatters: 83 | enable: 84 | - gci 85 | - gofumpt 86 | settings: 87 | gci: 88 | sections: 89 | - standard 90 | - default 91 | - prefix(github.com/docker) 92 | exclusions: 93 | generated: lax 94 | paths: 95 | - third_party$ 96 | - builtin$ 97 | - examples$ 98 | -------------------------------------------------------------------------------- /container/wait/file_test.go: -------------------------------------------------------------------------------- 1 | package wait_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "testing" 10 | "time" 11 | 12 | "github.com/containerd/errdefs" 13 | "github.com/moby/moby/api/types/container" 14 | "github.com/stretchr/testify/mock" 15 | "github.com/stretchr/testify/require" 16 | 17 | "github.com/docker/go-sdk/container/wait" 18 | ) 19 | 20 | const testFilename = "/tmp/file" 21 | 22 | var anyContext = mock.MatchedBy(func(_ context.Context) bool { return true }) 23 | 24 | // newRunningTarget creates a new mockStrategyTarget that is running. 25 | func newRunningTarget() *mockStrategyTarget { 26 | target := &mockStrategyTarget{} 27 | target.EXPECT().State(anyContext). 28 | Return(&container.State{Running: true}, nil) 29 | 30 | return target 31 | } 32 | 33 | // testForFile creates a new FileStrategy for testing. 34 | func testForFile() *wait.FileStrategy { 35 | return wait.ForFile(testFilename). 36 | WithTimeout(time.Millisecond * 50). 37 | WithPollInterval(time.Millisecond) 38 | } 39 | 40 | func TestForFile(t *testing.T) { 41 | errNotFound := errdefs.ErrNotFound.WithMessage("file not found") 42 | ctx := context.Background() 43 | 44 | t.Run("not-found", func(t *testing.T) { 45 | target := newRunningTarget() 46 | target.EXPECT().CopyFromContainer(anyContext, testFilename).Return(nil, errNotFound) 47 | err := testForFile().WaitUntilReady(ctx, target) 48 | require.EqualError(t, err, context.DeadlineExceeded.Error()) 49 | }) 50 | 51 | t.Run("other-error", func(t *testing.T) { 52 | otherErr := errors.New("other error") 53 | target := newRunningTarget() 54 | target.EXPECT().CopyFromContainer(anyContext, testFilename).Return(nil, otherErr) 55 | err := testForFile().WaitUntilReady(ctx, target) 56 | require.ErrorIs(t, err, otherErr) 57 | }) 58 | 59 | t.Run("valid", func(t *testing.T) { 60 | data := "my content\nwibble" 61 | file := bytes.NewBufferString(data) 62 | target := newRunningTarget() 63 | target.EXPECT().CopyFromContainer(anyContext, testFilename).Once().Return(nil, errNotFound) 64 | target.EXPECT().CopyFromContainer(anyContext, testFilename).Return(io.NopCloser(file), nil) 65 | var out bytes.Buffer 66 | err := testForFile().WithMatcher(func(r io.Reader) error { 67 | if _, err := io.Copy(&out, r); err != nil { 68 | return fmt.Errorf("copy: %w", err) 69 | } 70 | return nil 71 | }).WaitUntilReady(ctx, target) 72 | require.NoError(t, err) 73 | require.Equal(t, data, out.String()) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /client/client.container_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "sync" 8 | "testing" 9 | 10 | dockerclient "github.com/moby/moby/client" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/docker/go-sdk/client" 14 | ) 15 | 16 | func BenchmarkContainerList(b *testing.B) { 17 | dockerClient, err := client.New(context.Background()) 18 | require.NoError(b, err) 19 | require.NotNil(b, dockerClient) 20 | 21 | img := "nginx:alpine" 22 | 23 | pullImage(b, dockerClient, img) 24 | 25 | max := 5 26 | 27 | wg := sync.WaitGroup{} 28 | wg.Add(max) 29 | 30 | for i := range max { 31 | go func(i int) { 32 | defer wg.Done() 33 | 34 | createContainer(b, dockerClient, img, fmt.Sprintf("nginx-test-name-%d", i)) 35 | }(i) 36 | } 37 | 38 | wg.Wait() 39 | 40 | b.Run("container-list", func(b *testing.B) { 41 | b.ResetTimer() 42 | b.ReportAllocs() 43 | b.RunParallel(func(pb *testing.PB) { 44 | for pb.Next() { 45 | _, err := dockerClient.ContainerList(context.Background(), dockerclient.ContainerListOptions{All: true}) 46 | require.NoError(b, err) 47 | } 48 | }) 49 | }) 50 | 51 | b.Run("find-container-by-name", func(b *testing.B) { 52 | b.ResetTimer() 53 | b.ReportAllocs() 54 | b.RunParallel(func(pb *testing.PB) { 55 | for pb.Next() { 56 | _, err := dockerClient.FindContainerByName(context.Background(), fmt.Sprintf("nginx-test-name-%d", rand.Intn(max))) 57 | require.NoError(b, err) 58 | } 59 | }) 60 | }) 61 | } 62 | 63 | func BenchmarkContainerPause(b *testing.B) { 64 | dockerClient, err := client.New(context.Background()) 65 | require.NoError(b, err) 66 | require.NotNil(b, dockerClient) 67 | 68 | img := "nginx:alpine" 69 | 70 | containerName := "nginx-test-pause" 71 | 72 | pullImage(b, dockerClient, img) 73 | createContainer(b, dockerClient, img, containerName) 74 | 75 | b.Run("container-pause-unpause", func(b *testing.B) { 76 | _, err = dockerClient.ContainerStart(context.Background(), containerName, dockerclient.ContainerStartOptions{}) 77 | require.NoError(b, err) 78 | 79 | b.ResetTimer() 80 | b.ReportAllocs() 81 | for i := 0; i < b.N; i++ { 82 | _, err := dockerClient.ContainerPause(context.Background(), containerName, dockerclient.ContainerPauseOptions{}) 83 | require.NoError(b, err) 84 | 85 | _, err = dockerClient.ContainerUnpause(context.Background(), containerName, dockerclient.ContainerUnpauseOptions{}) 86 | require.NoError(b, err) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /container/testing.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/containerd/errdefs" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // errAlreadyInProgress is a regular expression that matches the error for a container 12 | // removal that is already in progress. 13 | var errAlreadyInProgress = regexp.MustCompile(`removal of container .* is already in progress`) 14 | 15 | // causer is an interface that allows to get the cause of an error. 16 | type causer interface { 17 | Cause() error 18 | } 19 | 20 | // wrapErr is an interface that allows to unwrap an error. 21 | type wrapErr interface { 22 | Unwrap() error 23 | } 24 | 25 | // unwrapErrs is an interface that allows to unwrap multiple errors. 26 | type unwrapErrs interface { 27 | Unwrap() []error 28 | } 29 | 30 | // Cleanup is a helper function that schedules a [TerminableContainer] 31 | // to be terminated when the test ends. 32 | // 33 | // This should be called directly after (before any error check) 34 | // [Create](...) in a test to ensure the 35 | // container is pruned when the function ends. 36 | // If the container is nil, it's a no-op. 37 | func Cleanup(tb testing.TB, ctr TerminableContainer, options ...TerminateOption) { 38 | tb.Helper() 39 | 40 | tb.Cleanup(func() { 41 | noErrorOrIgnored(tb, Terminate(ctr, options...)) 42 | }) 43 | } 44 | 45 | // isCleanupSafe checks if an error is cleanup safe. 46 | func isCleanupSafe(err error) bool { 47 | if err == nil { 48 | return true 49 | } 50 | 51 | // First try with containerd's errdefs 52 | switch { 53 | case errdefs.IsNotFound(err): 54 | return true 55 | case errdefs.IsConflict(err): 56 | // Terminating a container that is already terminating. 57 | if errAlreadyInProgress.MatchString(err.Error()) { 58 | return true 59 | } 60 | return false 61 | } 62 | 63 | switch x := err.(type) { //nolint:errorlint // We need to check for interfaces. 64 | case causer: 65 | return isCleanupSafe(x.Cause()) 66 | case wrapErr: 67 | return isCleanupSafe(x.Unwrap()) 68 | case unwrapErrs: 69 | for _, e := range x.Unwrap() { 70 | if !isCleanupSafe(e) { 71 | return false 72 | } 73 | } 74 | return true 75 | default: 76 | return false 77 | } 78 | } 79 | 80 | // noErrorOrIgnored is a helper function that checks if the error is nil or an error 81 | // we can ignore. 82 | func noErrorOrIgnored(tb testing.TB, err error) { 83 | tb.Helper() 84 | 85 | if isCleanupSafe(err) { 86 | return 87 | } 88 | 89 | require.NoError(tb, err) 90 | } 91 | -------------------------------------------------------------------------------- /container/wait/nop.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/moby/moby/api/types/container" 10 | "github.com/moby/moby/api/types/network" 11 | "github.com/moby/moby/client" 12 | 13 | "github.com/docker/go-sdk/container/exec" 14 | ) 15 | 16 | var ( 17 | _ Strategy = (*NopStrategy)(nil) 18 | _ StrategyTimeout = (*NopStrategy)(nil) 19 | noopLogger = slog.New(slog.NewTextHandler(io.Discard, nil)) 20 | ) 21 | 22 | type NopStrategy struct { 23 | timeout *time.Duration 24 | waitUntilReady func(context.Context, StrategyTarget) error 25 | } 26 | 27 | func ForNop( 28 | waitUntilReady func(context.Context, StrategyTarget) error, 29 | ) *NopStrategy { 30 | return &NopStrategy{ 31 | waitUntilReady: waitUntilReady, 32 | } 33 | } 34 | 35 | func (ws *NopStrategy) Timeout() *time.Duration { 36 | return ws.timeout 37 | } 38 | 39 | // String returns a human-readable description of the wait strategy. 40 | func (ws *NopStrategy) String() string { 41 | return "custom wait condition" 42 | } 43 | 44 | func (ws *NopStrategy) WithTimeout(timeout time.Duration) *NopStrategy { 45 | ws.timeout = &timeout 46 | return ws 47 | } 48 | 49 | func (ws *NopStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { 50 | return ws.waitUntilReady(ctx, target) 51 | } 52 | 53 | type NopStrategyTarget struct { 54 | ReaderCloser io.ReadCloser 55 | ContainerState container.State 56 | } 57 | 58 | func (st *NopStrategyTarget) Host(_ context.Context) (string, error) { 59 | return "", nil 60 | } 61 | 62 | func (st *NopStrategyTarget) Inspect(_ context.Context) (client.ContainerInspectResult, error) { 63 | return client.ContainerInspectResult{}, nil 64 | } 65 | 66 | func (st *NopStrategyTarget) MappedPort(_ context.Context, n network.Port) (network.Port, error) { 67 | return n, nil 68 | } 69 | 70 | func (st *NopStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) { 71 | return st.ReaderCloser, nil 72 | } 73 | 74 | func (st *NopStrategyTarget) Exec(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) { 75 | return 0, nil, nil 76 | } 77 | 78 | func (st *NopStrategyTarget) State(_ context.Context) (*container.State, error) { 79 | return &st.ContainerState, nil 80 | } 81 | 82 | func (st *NopStrategyTarget) CopyFromContainer(_ context.Context, _ string) (io.ReadCloser, error) { 83 | return st.ReaderCloser, nil 84 | } 85 | 86 | func (st *NopStrategyTarget) Logger() *slog.Logger { 87 | return noopLogger 88 | } 89 | -------------------------------------------------------------------------------- /container/ports.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | 9 | "github.com/containerd/errdefs" 10 | "github.com/moby/moby/api/types/network" 11 | ) 12 | 13 | // Endpoint gets proto://host:port string for the lowest numbered exposed port 14 | // Will return just host:port if proto is empty 15 | func (c *Container) Endpoint(ctx context.Context, proto string) (string, error) { 16 | inspect, err := c.Inspect(ctx) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | if len(inspect.Container.NetworkSettings.Ports) == 0 { 22 | return "", errdefs.ErrNotFound.WithMessage("no ports exposed") 23 | } 24 | 25 | // Get lowest numbered bound port. 26 | var lowestPort network.Port 27 | for port := range inspect.Container.NetworkSettings.Ports { 28 | if lowestPort.IsZero() || port.Num() < lowestPort.Num() { 29 | lowestPort = port 30 | } 31 | } 32 | 33 | return c.PortEndpoint(ctx, lowestPort, proto) 34 | } 35 | 36 | // PortEndpoint gets proto://host:port string for the given exposed port 37 | // It returns proto://host:port or proto://[IPv6host]:port string for the given exposed port. 38 | // It returns just host:port or [IPv6host]:port if proto is blank. 39 | // 40 | // TODO(robmry) - remove proto and use port.Proto() 41 | func (c *Container) PortEndpoint(ctx context.Context, port network.Port, proto string) (string, error) { 42 | host, err := c.Host(ctx) 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | outerPort, err := c.MappedPort(ctx, port) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | hostPort := net.JoinHostPort(host, strconv.Itoa(int(outerPort.Num()))) 53 | if proto == "" { 54 | return hostPort, nil 55 | } 56 | 57 | return proto + "://" + hostPort, nil 58 | } 59 | 60 | // MappedPort gets externally mapped port for a container port 61 | func (c *Container) MappedPort(ctx context.Context, port network.Port) (network.Port, error) { 62 | inspect, err := c.Inspect(ctx) 63 | if err != nil { 64 | return network.Port{}, fmt.Errorf("inspect: %w", err) 65 | } 66 | if inspect.Container.HostConfig.NetworkMode == "host" { 67 | return port, nil 68 | } 69 | 70 | ports := inspect.Container.NetworkSettings.Ports 71 | 72 | for k, p := range ports { 73 | if k != port { 74 | continue 75 | } 76 | if port.Proto() != "" && k.Proto() != port.Proto() { 77 | continue 78 | } 79 | if len(p) == 0 { 80 | continue 81 | } 82 | return network.ParsePort(p[0].HostPort + "/" + string(k.Proto())) 83 | } 84 | 85 | return network.Port{}, errdefs.ErrNotFound.WithMessage(fmt.Sprintf("port %q not found", port)) 86 | } 87 | -------------------------------------------------------------------------------- /image/pull_examples_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log" 9 | "strings" 10 | 11 | dockerclient "github.com/moby/moby/client" 12 | v1 "github.com/opencontainers/image-spec/specs-go/v1" 13 | 14 | "github.com/docker/go-sdk/client" 15 | "github.com/docker/go-sdk/image" 16 | ) 17 | 18 | func ExamplePull() { 19 | err := image.Pull(context.Background(), "nginx:latest") 20 | 21 | fmt.Println(err) 22 | 23 | // Output: 24 | // 25 | } 26 | 27 | func ExamplePull_withClient() { 28 | dockerClient, err := client.New(context.Background()) 29 | if err != nil { 30 | log.Printf("error creating client: %s", err) 31 | return 32 | } 33 | defer dockerClient.Close() 34 | 35 | err = image.Pull(context.Background(), "nginx:latest", image.WithPullClient(dockerClient)) 36 | 37 | fmt.Println(err) 38 | 39 | // Output: 40 | // 41 | } 42 | 43 | func ExamplePull_withPullOptions() { 44 | opts := dockerclient.ImagePullOptions{Platforms: []v1.Platform{ 45 | { 46 | OS: "linux", 47 | Architecture: "amd64", 48 | }, 49 | }} 50 | 51 | err := image.Pull(context.Background(), "alpine:3.22", image.WithPullOptions(opts)) 52 | 53 | fmt.Println(err) 54 | 55 | // Output: 56 | // 57 | } 58 | 59 | func ExamplePull_withPullHandler() { 60 | opts := dockerclient.ImagePullOptions{Platforms: []v1.Platform{ 61 | { 62 | OS: "linux", 63 | Architecture: "amd64", 64 | }, 65 | }} 66 | 67 | buff := &bytes.Buffer{} 68 | 69 | err := image.Pull(context.Background(), "alpine:3.22", image.WithPullOptions(opts), image.WithPullHandler(func(r io.ReadCloser) error { 70 | _, err := io.Copy(buff, r) 71 | return err 72 | })) 73 | 74 | fmt.Println(err) 75 | fmt.Println(strings.Contains(buff.String(), "Pulling from library/alpine")) 76 | 77 | // Output: 78 | // 79 | // true 80 | } 81 | 82 | func ExampleDisplayProgress() { 83 | // Display formatted pull progress to a custom writer (buffer in this example). 84 | // Verifies that DisplayProgress formats the output (not raw JSON). 85 | buff := &bytes.Buffer{} 86 | 87 | err := image.Pull(context.Background(), "nginx:latest", 88 | image.WithPullHandler(image.DisplayProgress(buff))) 89 | 90 | fmt.Println(err) 91 | // Every single message from Docker starts with { and contains ", 92 | // so raw JSON will always have {", while formatted output strips 93 | // away the JSON structure. 94 | fmt.Println(!strings.Contains(buff.String(), "{\"")) 95 | 96 | // Output: 97 | // 98 | // true 99 | } 100 | -------------------------------------------------------------------------------- /container/wait/exit.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // Implement interface 10 | var ( 11 | _ Strategy = (*ExitStrategy)(nil) 12 | _ StrategyTimeout = (*ExitStrategy)(nil) 13 | ) 14 | 15 | // ExitStrategy will wait until container exit 16 | type ExitStrategy struct { 17 | // all Strategies should have a timeout to avoid waiting infinitely 18 | timeout *time.Duration 19 | 20 | // additional properties 21 | PollInterval time.Duration 22 | } 23 | 24 | // NewExitStrategy constructs with polling interval of 100 milliseconds without timeout by default 25 | func NewExitStrategy() *ExitStrategy { 26 | return &ExitStrategy{ 27 | PollInterval: defaultPollInterval(), 28 | } 29 | } 30 | 31 | // fluent builders for each property 32 | // since go has neither covariance nor generics, the return type must be the type of the concrete implementation 33 | // this is true for all properties, even the "shared" ones 34 | 35 | // WithTimeout can be used to change the default exit timeout 36 | func (ws *ExitStrategy) WithTimeout(exitTimeout time.Duration) *ExitStrategy { 37 | ws.timeout = &exitTimeout 38 | return ws 39 | } 40 | 41 | // WithPollInterval can be used to override the default polling interval of 100 milliseconds 42 | func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy { 43 | ws.PollInterval = pollInterval 44 | return ws 45 | } 46 | 47 | // ForExit is the default construction for the fluid interface. 48 | // 49 | // For Example: 50 | // 51 | // wait. 52 | // ForExit(). 53 | // WithPollInterval(1 * time.Second) 54 | func ForExit() *ExitStrategy { 55 | return NewExitStrategy() 56 | } 57 | 58 | func (ws *ExitStrategy) Timeout() *time.Duration { 59 | return ws.timeout 60 | } 61 | 62 | // String returns a human-readable description of the wait strategy. 63 | func (ws *ExitStrategy) String() string { 64 | return "container to exit" 65 | } 66 | 67 | // WaitUntilReady implements Strategy.WaitUntilReady 68 | func (ws *ExitStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { 69 | if ws.timeout != nil { 70 | var cancel context.CancelFunc 71 | ctx, cancel = context.WithTimeout(ctx, *ws.timeout) 72 | defer cancel() 73 | } 74 | 75 | for { 76 | select { 77 | case <-ctx.Done(): 78 | return ctx.Err() 79 | default: 80 | state, err := target.State(ctx) 81 | if err != nil { 82 | if !strings.Contains(err.Error(), "No such container") { 83 | return err 84 | } 85 | return nil 86 | } 87 | if state.Running { 88 | time.Sleep(ws.PollInterval) 89 | continue 90 | } 91 | return nil 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | detect-packages: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | pull-requests: read 17 | outputs: 18 | packages: ${{ steps.filter.outputs.changes || '[]' }} 19 | steps: 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | 23 | - name: Generate filters 24 | id: filter-setup 25 | run: | 26 | filters=$(go work edit -json | jq -r '.Use[] | "\(.DiskPath | ltrimstr("./")): \"\(.DiskPath | ltrimstr("./"))/**\""') 27 | echo "filters<> $GITHUB_OUTPUT 28 | echo "$filters" >> $GITHUB_OUTPUT 29 | echo "EOF" >> $GITHUB_OUTPUT 30 | shell: bash 31 | 32 | - name: Filter changes 33 | id: filter 34 | uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 #v3 35 | with: 36 | filters: ${{ steps.filter-setup.outputs.filters }} 37 | 38 | release-drafter: 39 | needs: detect-packages 40 | runs-on: ubuntu-latest 41 | timeout-minutes: 30 42 | if: needs.detect-packages.outputs.packages != '[]' # Ensure job runs only if there are changes 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | permissions: 46 | contents: write # for release-drafter/release-drafter to create a github release 47 | strategy: 48 | matrix: 49 | package: ${{ fromJSON(needs.detect-packages.outputs.packages || '[]') }} 50 | steps: 51 | - name: Check out code into the Go module directory 52 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 53 | 54 | - name: Generate dynamic config from template 55 | id: generate-config 56 | env: 57 | PACKAGE: ${{ matrix.package }} 58 | run: | 59 | folder="${PACKAGE}" 60 | sed "s|{{FOLDER}}|$folder|g" .github/release-drafter-template.yml > .github/release-drafter-$folder.yml 61 | echo "config<> $GITHUB_OUTPUT 62 | cat .github/release-drafter-$folder.yml >> $GITHUB_OUTPUT 63 | echo "EOF" >> $GITHUB_OUTPUT 64 | 65 | # Workaround until https://github.com/release-drafter/release-drafter/pull/1423 is merged 66 | - name: Use dynamic release-drafter configuration 67 | uses: ReneWerner87/release-drafter@6dec4ceb1fb86b6514f11a2e7a39e1dedce709d0 68 | with: 69 | config: ${{ steps.generate-config.outputs.config }} 70 | -------------------------------------------------------------------------------- /container/image_substitutors_test.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestImageSubstitutors(t *testing.T) { 10 | t.Run("custom-hub", func(t *testing.T) { 11 | t.Run("prepend-registry", func(t *testing.T) { 12 | s := NewCustomHubSubstitutor("quay.io") 13 | 14 | img, err := s.Substitute("foo/foo:latest") 15 | require.NoError(t, err) 16 | 17 | require.Equal(t, "quay.io/foo/foo:latest", img) 18 | }) 19 | 20 | t.Run("no-prepend-same-registry", func(t *testing.T) { 21 | s := NewCustomHubSubstitutor("quay.io") 22 | 23 | img, err := s.Substitute("quay.io/foo/foo:latest") 24 | require.NoError(t, err) 25 | 26 | require.Equal(t, "quay.io/foo/foo:latest", img) 27 | }) 28 | }) 29 | 30 | t.Run("docker-hub", func(t *testing.T) { 31 | t.Run("prepend-registry", func(t *testing.T) { 32 | t.Run("image", func(t *testing.T) { 33 | s := newPrependHubRegistry("my-registry") 34 | 35 | img, err := s.Substitute("foo:latest") 36 | require.NoError(t, err) 37 | 38 | require.Equal(t, "my-registry/foo:latest", img) 39 | }) 40 | t.Run("image/user", func(t *testing.T) { 41 | s := newPrependHubRegistry("my-registry") 42 | 43 | img, err := s.Substitute("user/foo:latest") 44 | require.NoError(t, err) 45 | 46 | require.Equal(t, "my-registry/user/foo:latest", img) 47 | }) 48 | 49 | t.Run("image/organization/user", func(t *testing.T) { 50 | s := newPrependHubRegistry("my-registry") 51 | 52 | img, err := s.Substitute("org/user/foo:latest") 53 | require.NoError(t, err) 54 | 55 | require.Equal(t, "my-registry/org/user/foo:latest", img) 56 | }) 57 | }) 58 | 59 | t.Run("no-prepend-registry", func(t *testing.T) { 60 | t.Run("non-hub-image", func(t *testing.T) { 61 | s := newPrependHubRegistry("my-registry") 62 | 63 | img, err := s.Substitute("quay.io/foo:latest") 64 | require.NoError(t, err) 65 | 66 | require.Equal(t, "quay.io/foo:latest", img) 67 | }) 68 | 69 | t.Run("registry.hub.docker.com/library", func(t *testing.T) { 70 | s := newPrependHubRegistry("my-registry") 71 | 72 | img, err := s.Substitute("registry.hub.docker.com/library/foo:latest") 73 | require.NoError(t, err) 74 | 75 | require.Equal(t, "registry.hub.docker.com/library/foo:latest", img) 76 | }) 77 | 78 | t.Run("registry.hub.docker.com", func(t *testing.T) { 79 | s := newPrependHubRegistry("my-registry") 80 | 81 | img, err := s.Substitute("registry.hub.docker.com/foo:latest") 82 | require.NoError(t, err) 83 | 84 | require.Equal(t, "registry.hub.docker.com/foo:latest", img) 85 | }) 86 | }) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /context/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 5 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 6 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 7 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= 13 | github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= 14 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 15 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 16 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 17 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 21 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 22 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 23 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 26 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 30 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 31 | -------------------------------------------------------------------------------- /image/build_unit_test.go: -------------------------------------------------------------------------------- 1 | package image 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "log/slog" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/containerd/errdefs" 13 | dockerclient "github.com/moby/moby/client" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/docker/go-sdk/client" 17 | ) 18 | 19 | func TestBuild_withRetries(t *testing.T) { 20 | testBuild := func(t *testing.T, errReturned error, shouldRetry bool) { 21 | t.Helper() 22 | 23 | buf := &bytes.Buffer{} 24 | logger := slog.New(slog.NewTextHandler(buf, nil)) 25 | m := &errMockCli{err: errReturned} 26 | 27 | sdk, err := client.New(context.TODO(), client.WithDockerAPI(m), client.WithLogger(logger)) 28 | require.NoError(t, err) 29 | 30 | contextArchive, err := ArchiveBuildContext("testdata/retry", "Dockerfile") 31 | require.NoError(t, err) 32 | 33 | // give a chance to retry 34 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 35 | defer cancel() 36 | tag, err := Build( 37 | ctx, contextArchive, "test", 38 | WithBuildClient(sdk), 39 | WithBuildOptions(dockerclient.ImageBuildOptions{ 40 | Dockerfile: "Dockerfile", 41 | }), 42 | ) 43 | if errReturned != nil { 44 | require.Error(t, err) 45 | } else { 46 | require.NoError(t, err) 47 | require.Equal(t, "test", tag) 48 | } 49 | 50 | require.Positive(t, m.imageBuildCount) 51 | require.Equal(t, shouldRetry, m.imageBuildCount > 1) 52 | 53 | s := buf.String() 54 | require.Equal(t, shouldRetry, strings.Contains(s, "Failed to build image, will retry")) 55 | } 56 | 57 | t.Run("success/no-retry", func(t *testing.T) { 58 | testBuild(t, nil, false) 59 | }) 60 | 61 | t.Run("not-available/no-retry", func(t *testing.T) { 62 | testBuild(t, errdefs.ErrNotFound.WithMessage("not available"), false) 63 | }) 64 | 65 | t.Run("invalid-parameters/no-retry", func(t *testing.T) { 66 | testBuild(t, errdefs.ErrInvalidArgument.WithMessage("invalid"), false) 67 | }) 68 | 69 | t.Run("unauthorized/no-retry", func(t *testing.T) { 70 | testBuild(t, errdefs.ErrUnauthenticated.WithMessage("not authorized"), false) 71 | }) 72 | 73 | t.Run("forbidden/no-retry", func(t *testing.T) { 74 | testBuild(t, errdefs.ErrPermissionDenied.WithMessage("forbidden"), false) 75 | }) 76 | 77 | t.Run("not-implemented/no-retry", func(t *testing.T) { 78 | testBuild(t, errdefs.ErrNotImplemented.WithMessage("unknown method"), false) 79 | }) 80 | 81 | t.Run("system-error/no-retry", func(t *testing.T) { 82 | testBuild(t, errdefs.ErrInternal.WithMessage("system error"), false) 83 | }) 84 | 85 | t.Run("permanent-error/retry", func(t *testing.T) { 86 | testBuild(t, errors.New("whoops"), true) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /context/context.add_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/docker/go-sdk/context" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | t.Run("empty-name", func(tt *testing.T) { 13 | _, err := context.New("") 14 | require.Error(tt, err) 15 | }) 16 | 17 | t.Run("default-name", func(tt *testing.T) { 18 | _, err := context.New("default") 19 | require.Error(tt, err) 20 | }) 21 | 22 | t.Run("error/meta-root", func(tt *testing.T) { 23 | tt.Setenv("HOME", tt.TempDir()) 24 | tt.Setenv("USERPROFILE", tt.TempDir()) // Windows support 25 | 26 | _, err := context.New("test") 27 | require.Error(tt, err) 28 | }) 29 | 30 | t.Run("success", func(t *testing.T) { 31 | context.SetupTestDockerContexts(t, 1, 3) 32 | 33 | t.Run("no-current", func(tt *testing.T) { 34 | ctx, err := context.New( 35 | "test1234", 36 | context.WithHost("tcp://127.0.0.1:1234"), 37 | context.WithDescription("test description"), 38 | context.WithAdditionalFields(map[string]any{"testKey": "testValue"}), 39 | ) 40 | require.NoError(tt, err) 41 | defer func() { 42 | require.NoError(tt, ctx.Delete()) 43 | }() 44 | 45 | list, err := context.List() 46 | require.NoError(tt, err) 47 | require.Contains(tt, list, ctx.Name) 48 | 49 | require.Equal(tt, "test1234", ctx.Name) 50 | require.Equal(tt, "test description", ctx.Metadata.Description) 51 | require.Equal(tt, "tcp://127.0.0.1:1234", ctx.Endpoints["docker"].Host) 52 | require.False(tt, ctx.Endpoints["docker"].SkipTLSVerify) 53 | 54 | fields := ctx.Metadata.Fields() 55 | require.Equal(tt, map[string]any{"testKey": "testValue"}, fields) 56 | 57 | value, exists := fields["testKey"] 58 | require.True(tt, exists) 59 | require.Equal(tt, "testValue", value) 60 | 61 | // the current context is not the new one 62 | current, err := context.Current() 63 | require.NoError(tt, err) 64 | require.NotEqual(tt, "test1234", current) 65 | }) 66 | 67 | t.Run("as-current", func(tt *testing.T) { 68 | ctx, err := context.New("test1234", context.WithHost("tcp://127.0.0.1:1234"), context.AsCurrent()) 69 | require.NoError(tt, err) 70 | defer func() { 71 | require.NoError(tt, ctx.Delete()) 72 | }() 73 | 74 | list, err := context.List() 75 | require.NoError(tt, err) 76 | require.Contains(tt, list, ctx.Name) 77 | 78 | require.Equal(tt, "test1234", ctx.Name) 79 | require.Equal(tt, "tcp://127.0.0.1:1234", ctx.Endpoints["docker"].Host) 80 | require.False(tt, ctx.Endpoints["docker"].SkipTLSVerify) 81 | 82 | // the current context is the new one 83 | current, err := context.Current() 84 | require.NoError(tt, err) 85 | require.Equal(tt, "test1234", current) 86 | }) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/release-modules.yml: -------------------------------------------------------------------------------- 1 | name: Release All Modules 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | dry_run: 7 | description: 'Perform a dry run without creating tags or pushing changes' 8 | required: false 9 | default: true 10 | type: boolean 11 | bump_type: 12 | description: 'Type of version bump to perform' 13 | required: false 14 | default: 'prerelease' 15 | type: choice 16 | options: 17 | - prerelease 18 | - patch 19 | - minor 20 | - major 21 | 22 | jobs: 23 | release-all-modules: 24 | # Additional safety check - only run on main branch 25 | if: ${{ github.ref == 'refs/heads/main' }} 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: write 29 | 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | with: 34 | fetch-depth: 0 # Fetch all history and tags 35 | token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Verify branch 38 | run: | 39 | if [[ "${{ github.ref_name }}" != "main" ]]; then 40 | echo "❌ Releases can only be performed from the main branch" 41 | echo "Current branch: ${{ github.ref_name }}" 42 | exit 1 43 | fi 44 | echo "✅ Running release from main branch" 45 | 46 | - name: Set up Go 47 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 48 | with: 49 | go-version-file: 'go.work' 50 | 51 | - name: Configure Git 52 | if: ${{ !inputs.dry_run }} 53 | run: | 54 | git config --global user.name "github-actions[bot]" 55 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 56 | 57 | - name: Display run configuration 58 | run: | 59 | echo "🚀 Release Configuration:" 60 | echo " - Dry Run: ${{ inputs.dry_run }}" 61 | echo " - Bump Type: ${{ inputs.bump_type }}" 62 | echo " - Repository: ${{ github.repository }}" 63 | echo " - Branch: ${{ github.ref_name }}" 64 | 65 | - name: Run release for all modules 66 | env: 67 | DRY_RUN: ${{ inputs.dry_run }} 68 | BUMP_TYPE: ${{ inputs.bump_type }} 69 | LOCAL_RELEASE: false 70 | run: | 71 | echo "Starting release process..." 72 | make release-all 73 | 74 | if [[ "${{ inputs.dry_run }}" == "true" ]]; then 75 | echo "✅ Dry run release completed successfully!" 76 | echo "No changes were made to the repository." 77 | else 78 | echo "✅ Release completed successfully!" 79 | echo "All modules have been updated, tags have been created and Go proxy has been updated" 80 | fi 81 | -------------------------------------------------------------------------------- /container/container.stop.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/moby/moby/client" 9 | ) 10 | 11 | // StopOptions is a type that holds the options for stopping a container. 12 | type StopOptions struct { 13 | ctx context.Context 14 | stopTimeout time.Duration 15 | } 16 | 17 | // StopOption is a type that represents an option for stopping a container. 18 | type StopOption func(*StopOptions) 19 | 20 | // Context returns the context to use during a Stop or Terminate. 21 | func (o *StopOptions) Context() context.Context { 22 | return o.ctx 23 | } 24 | 25 | // StopTimeout returns the stop timeout to use during a Stop or Terminate. 26 | func (o *StopOptions) StopTimeout() time.Duration { 27 | return o.stopTimeout 28 | } 29 | 30 | // StopTimeout returns a StopOption that sets the timeout. 31 | // Default: See [Container.Stop]. 32 | func StopTimeout(timeout time.Duration) StopOption { 33 | return func(c *StopOptions) { 34 | c.stopTimeout = timeout 35 | } 36 | } 37 | 38 | // NewStopOptions returns a fully initialised StopOptions. 39 | // Defaults: StopTimeout: 10 seconds. 40 | func NewStopOptions(ctx context.Context, opts ...StopOption) *StopOptions { 41 | options := &StopOptions{ 42 | stopTimeout: time.Second * 10, 43 | ctx: ctx, 44 | } 45 | for _, opt := range opts { 46 | opt(options) 47 | } 48 | return options 49 | } 50 | 51 | // Stop stops the container. 52 | // 53 | // In case the container fails to stop gracefully within a time frame specified 54 | // by the timeout argument, it is forcefully terminated (killed). 55 | // 56 | // If no timeout is passed, the default StopTimeout value is used, 10 seconds, 57 | // otherwise the engine default. A negative timeout value can be specified, 58 | // meaning no timeout, i.e. no forceful termination is performed. 59 | // 60 | // All hooks are called in the following order: 61 | // - [LifecycleHooks.PreStops] 62 | // - [LifecycleHooks.PostStops] 63 | // 64 | // If the container is already stopped, the method is a no-op. 65 | func (c *Container) Stop(ctx context.Context, opts ...StopOption) error { 66 | stopOptions := NewStopOptions(ctx, opts...) 67 | 68 | err := c.stoppingHook(stopOptions.Context()) 69 | if err != nil { 70 | return fmt.Errorf("stopping hook: %w", err) 71 | } 72 | 73 | var options client.ContainerStopOptions 74 | 75 | timeoutSeconds := int(stopOptions.StopTimeout().Seconds()) 76 | options.Timeout = &timeoutSeconds 77 | 78 | if _, err := c.dockerClient.ContainerStop(stopOptions.Context(), c.ID(), options); err != nil { 79 | return fmt.Errorf("container stop: %w", err) 80 | } 81 | 82 | c.isRunning = false 83 | 84 | err = c.stoppedHook(stopOptions.Context()) 85 | if err != nil { 86 | return fmt.Errorf("stopped hook: %w", err) 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /container/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-sdk/container 2 | 3 | go 1.24.0 4 | 5 | replace ( 6 | github.com/docker/go-sdk/client => ../client 7 | github.com/docker/go-sdk/config => ../config 8 | github.com/docker/go-sdk/context => ../context 9 | github.com/docker/go-sdk/image => ../image 10 | github.com/docker/go-sdk/network => ../network 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.2 15 | github.com/containerd/errdefs v1.0.0 16 | github.com/containerd/platforms v0.2.1 17 | github.com/docker/go-connections v0.6.0 18 | github.com/docker/go-sdk/client v0.1.0-alpha011 19 | github.com/docker/go-sdk/config v0.1.0-alpha011 20 | github.com/docker/go-sdk/image v0.1.0-alpha012 21 | github.com/docker/go-sdk/network v0.1.0-alpha011 22 | github.com/moby/moby/api v1.52.0 23 | github.com/moby/moby/client v0.1.0 24 | github.com/stretchr/testify v1.11.1 25 | golang.org/x/sys v0.35.0 26 | ) 27 | 28 | require ( 29 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 30 | github.com/Microsoft/go-winio v0.6.2 // indirect 31 | github.com/caarlos0/env/v11 v11.3.1 // indirect 32 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 33 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 34 | github.com/containerd/log v0.1.0 // indirect 35 | github.com/davecgh/go-spew v1.1.1 // indirect 36 | github.com/distribution/reference v0.6.0 // indirect 37 | github.com/docker/go-sdk/context v0.1.0-alpha011 // indirect 38 | github.com/docker/go-units v0.5.0 // indirect 39 | github.com/felixge/httpsnoop v1.0.4 // indirect 40 | github.com/go-logr/logr v1.4.3 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/klauspost/compress v1.18.0 // indirect 44 | github.com/moby/docker-image-spec v1.3.1 // indirect 45 | github.com/moby/go-archive v0.1.0 // indirect 46 | github.com/moby/patternmatcher v0.6.0 // indirect 47 | github.com/moby/sys/sequential v0.6.0 // indirect 48 | github.com/moby/sys/user v0.4.0 // indirect 49 | github.com/moby/sys/userns v0.1.0 // indirect 50 | github.com/moby/term v0.5.2 // indirect 51 | github.com/opencontainers/go-digest v1.0.0 // indirect 52 | github.com/opencontainers/image-spec v1.1.1 // indirect 53 | github.com/pmezard/go-difflib v1.0.0 // indirect 54 | github.com/sirupsen/logrus v1.9.3 // indirect 55 | github.com/stretchr/objx v0.5.2 // indirect 56 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 57 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect 58 | go.opentelemetry.io/otel v1.38.0 // indirect 59 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 60 | go.opentelemetry.io/otel/sdk v1.38.0 // indirect 61 | go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect 62 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /container/wait/health.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/moby/moby/api/types/container" 8 | ) 9 | 10 | // Implement interface 11 | var ( 12 | _ Strategy = (*HealthStrategy)(nil) 13 | _ StrategyTimeout = (*HealthStrategy)(nil) 14 | ) 15 | 16 | // HealthStrategy will wait until the container becomes healthy 17 | type HealthStrategy struct { 18 | // all Strategies should have a startupTimeout to avoid waiting infinitely 19 | timeout *time.Duration 20 | 21 | // additional properties 22 | PollInterval time.Duration 23 | } 24 | 25 | // NewHealthStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default 26 | func NewHealthStrategy() *HealthStrategy { 27 | return &HealthStrategy{ 28 | PollInterval: defaultPollInterval(), 29 | } 30 | } 31 | 32 | // fluent builders for each property 33 | // since go has neither covariance nor generics, the return type must be the type of the concrete implementation 34 | // this is true for all properties, even the "shared" ones like startupTimeout 35 | 36 | // WithTimeout can be used to change the default startup timeout 37 | func (ws *HealthStrategy) WithTimeout(startupTimeout time.Duration) *HealthStrategy { 38 | ws.timeout = &startupTimeout 39 | return ws 40 | } 41 | 42 | // WithPollInterval can be used to override the default polling interval of 100 milliseconds 43 | func (ws *HealthStrategy) WithPollInterval(pollInterval time.Duration) *HealthStrategy { 44 | ws.PollInterval = pollInterval 45 | return ws 46 | } 47 | 48 | // ForHealthCheck is the default construction for the fluid interface. 49 | // 50 | // For Example: 51 | // 52 | // wait. 53 | // ForHealthCheck(). 54 | // WithPollInterval(1 * time.Second) 55 | func ForHealthCheck() *HealthStrategy { 56 | return NewHealthStrategy() 57 | } 58 | 59 | func (ws *HealthStrategy) Timeout() *time.Duration { 60 | return ws.timeout 61 | } 62 | 63 | // String returns a human-readable description of the wait strategy. 64 | func (ws *HealthStrategy) String() string { 65 | return "container to become healthy" 66 | } 67 | 68 | // WaitUntilReady implements Strategy.WaitUntilReady 69 | func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { 70 | timeout := defaultTimeout() 71 | if ws.timeout != nil { 72 | timeout = *ws.timeout 73 | } 74 | 75 | ctx, cancel := context.WithTimeout(ctx, timeout) 76 | defer cancel() 77 | 78 | for { 79 | select { 80 | case <-ctx.Done(): 81 | return ctx.Err() 82 | default: 83 | state, err := target.State(ctx) 84 | if err != nil { 85 | return err 86 | } 87 | if err := checkState(state); err != nil { 88 | return err 89 | } 90 | if state.Health == nil || state.Health.Status != container.Healthy { 91 | time.Sleep(ws.PollInterval) 92 | continue 93 | } 94 | return nil 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /context/context_examples_test.go: -------------------------------------------------------------------------------- 1 | package context_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/docker/go-sdk/context" 8 | ) 9 | 10 | func ExampleCurrent() { 11 | ctx, err := context.Current() 12 | fmt.Println(err) 13 | fmt.Println(ctx != "") 14 | 15 | // Output: 16 | // 17 | // true 18 | } 19 | 20 | func ExampleCurrentDockerHost() { 21 | host, err := context.CurrentDockerHost() 22 | fmt.Println(err) 23 | fmt.Println(host != "") 24 | 25 | // Output: 26 | // 27 | // true 28 | } 29 | 30 | func ExampleDockerHostFromContext() { 31 | host, err := context.DockerHostFromContext("desktop-linux") 32 | if err != nil { 33 | log.Printf("error getting docker host from context: %s", err) 34 | return 35 | } 36 | 37 | fmt.Println(host) 38 | 39 | // Intentionally not printing the output, as the context could not exist in the CI environment 40 | } 41 | 42 | func ExampleNew() { 43 | ctx, err := context.New("my-context") 44 | if err != nil { 45 | log.Printf("error adding context: %s", err) 46 | return 47 | } 48 | defer func() { 49 | if err := ctx.Delete(); err != nil { 50 | log.Printf("error deleting context: %s", err) 51 | } 52 | }() 53 | 54 | fmt.Println(ctx.Name) 55 | 56 | // Output: 57 | // my-context 58 | } 59 | 60 | func ExampleNew_asCurrent() { 61 | ctx, err := context.New("my-context", context.AsCurrent(), context.WithHost("tcp://127.0.0.1:2375")) 62 | if err != nil { 63 | log.Printf("error adding context: %s", err) 64 | return 65 | } 66 | defer func() { 67 | if err := ctx.Delete(); err != nil { 68 | log.Printf("error deleting context: %s", err) 69 | } 70 | }() 71 | 72 | fmt.Println(ctx.Name) 73 | 74 | current, err := context.Current() 75 | if err != nil { 76 | log.Printf("error getting current context: %s", err) 77 | return 78 | } 79 | fmt.Println(current) 80 | 81 | host, err := context.CurrentDockerHost() 82 | if err != nil { 83 | log.Printf("error getting current docker host: %s", err) 84 | return 85 | } 86 | 87 | fmt.Println(host) 88 | 89 | // Output: 90 | // my-context 91 | // my-context 92 | // tcp://127.0.0.1:2375 93 | } 94 | 95 | func ExampleList() { 96 | contexts, err := context.List() 97 | if err != nil { 98 | log.Printf("error listing contexts: %s", err) 99 | return 100 | } 101 | 102 | fmt.Println(contexts) 103 | 104 | // Intentionally not printing the output, as the contexts could not exist in the CI environment 105 | } 106 | 107 | func ExampleInspect() { 108 | ctx, err := context.Inspect("docker-cloud") 109 | if err != nil { 110 | log.Printf("error inspecting context: %s", err) 111 | return 112 | } 113 | 114 | fmt.Println(ctx.Metadata.Description) 115 | fmt.Println(ctx.Metadata.Field("otel")) 116 | fmt.Println(ctx.Metadata.Fields()) 117 | 118 | // Intentionally not printing the output, as the context could not exist in the CI environment 119 | } 120 | -------------------------------------------------------------------------------- /image/build_examples_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "log" 8 | "log/slog" 9 | "path" 10 | 11 | dockerclient "github.com/moby/moby/client" 12 | 13 | "github.com/docker/go-sdk/client" 14 | "github.com/docker/go-sdk/image" 15 | ) 16 | 17 | func ExampleBuild() { 18 | // using a buffer to capture the build output 19 | buf := &bytes.Buffer{} 20 | logger := slog.New(slog.NewTextHandler(buf, nil)) 21 | 22 | cli, err := client.New(context.Background(), client.WithLogger(logger)) 23 | if err != nil { 24 | log.Println("error creating docker client", err) 25 | return 26 | } 27 | defer func() { 28 | err := cli.Close() 29 | if err != nil { 30 | log.Println("error closing docker client", err) 31 | } 32 | }() 33 | 34 | buildPath := path.Join("testdata", "build") 35 | 36 | contextArchive, err := image.ArchiveBuildContext(buildPath, "Dockerfile") 37 | if err != nil { 38 | log.Println("error creating reader", err) 39 | return 40 | } 41 | 42 | tag, err := image.Build( 43 | context.Background(), contextArchive, "example:test", 44 | image.WithBuildOptions(dockerclient.ImageBuildOptions{ 45 | Dockerfile: "Dockerfile", 46 | }), 47 | ) 48 | if err != nil { 49 | log.Println("error building image", err) 50 | return 51 | } 52 | defer func() { 53 | _, err = image.Remove(context.Background(), tag, image.WithRemoveOptions(dockerclient.ImageRemoveOptions{ 54 | Force: true, 55 | PruneChildren: true, 56 | })) 57 | if err != nil { 58 | log.Println("error removing image", err) 59 | } 60 | }() 61 | 62 | fmt.Println(tag) 63 | 64 | // Output: 65 | // example:test 66 | } 67 | 68 | func ExampleBuildFromDir() { 69 | // using a buffer to capture the build output 70 | buf := &bytes.Buffer{} 71 | logger := slog.New(slog.NewTextHandler(buf, nil)) 72 | 73 | cli, err := client.New(context.Background(), client.WithLogger(logger)) 74 | if err != nil { 75 | log.Println("error creating docker client", err) 76 | return 77 | } 78 | defer func() { 79 | err := cli.Close() 80 | if err != nil { 81 | log.Println("error closing docker client", err) 82 | } 83 | }() 84 | 85 | buildPath := path.Join("testdata", "build") 86 | 87 | tag, err := image.BuildFromDir( 88 | context.Background(), buildPath, "Dockerfile", "example:test", 89 | image.WithBuildOptions(dockerclient.ImageBuildOptions{ 90 | Dockerfile: "Dockerfile", 91 | }), 92 | ) 93 | if err != nil { 94 | log.Println("error building image", err) 95 | return 96 | } 97 | defer func() { 98 | _, err = image.Remove(context.Background(), tag, image.WithRemoveOptions(dockerclient.ImageRemoveOptions{ 99 | Force: true, 100 | PruneChildren: true, 101 | })) 102 | if err != nil { 103 | log.Println("error removing image", err) 104 | } 105 | }() 106 | 107 | fmt.Println(tag) 108 | 109 | // Output: 110 | // example:test 111 | } 112 | -------------------------------------------------------------------------------- /image/dockerfiles_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/docker/go-sdk/image" 10 | ) 11 | 12 | func TestExtractImagesFromDockerfile(t *testing.T) { 13 | baseImage := "scratch" 14 | registryHost := "localhost" 15 | registryPort := "5000" 16 | nginxImage := "nginx:latest" 17 | 18 | extractImages := func(t *testing.T, dockerfile string, buildArgs map[string]*string, expected []string, expectedError bool) { 19 | t.Helper() 20 | 21 | images, err := image.ImagesFromDockerfile(dockerfile, buildArgs) 22 | if expectedError { 23 | require.Error(t, err) 24 | require.Empty(t, images) 25 | } else { 26 | require.NoError(t, err) 27 | require.Equal(t, expected, images) 28 | } 29 | } 30 | 31 | t.Run("wrong-file", func(t *testing.T) { 32 | extractImages(t, "", nil, []string{}, true) 33 | }) 34 | 35 | t.Run("single-image", func(t *testing.T) { 36 | extractImages(t, filepath.Join("testdata", "Dockerfile"), nil, []string{"nginx:${tag}"}, false) 37 | }) 38 | 39 | t.Run("multiple-images", func(t *testing.T) { 40 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage"), nil, []string{"nginx:a", "nginx:b", "nginx:c", "scratch"}, false) 41 | }) 42 | 43 | t.Run("multiple-images-with-one-build-arg", func(t *testing.T) { 44 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage.singleBuildArgs"), map[string]*string{"BASE_IMAGE": &baseImage}, []string{"nginx:a", "nginx:b", "nginx:c", "scratch"}, false) 45 | }) 46 | 47 | t.Run("multiple-images-with-one-build-arg-defaults", func(t *testing.T) { 48 | // no build args provided, so the default value should be used 49 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage.singleBuildArgs.defaults"), map[string]*string{"BASE_IMAGE": nil}, []string{"nginx:a", "nginx:b", "nginx:c", "nginx:d"}, false) 50 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage.singleBuildArgs.defaults"), map[string]*string{}, []string{"nginx:a", "nginx:b", "nginx:c", "nginx:d"}, false) 51 | 52 | // build arg provided, but not the default value 53 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage.singleBuildArgs.defaults"), map[string]*string{"BASE_IMAGE": &baseImage}, []string{"nginx:a", "nginx:b", "nginx:c", "scratch"}, false) 54 | 55 | // build arg provided, and the default value 56 | nginxZ := "nginx:z" 57 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage.singleBuildArgs.defaults"), map[string]*string{"BASE_IMAGE": &nginxZ}, []string{"nginx:a", "nginx:b", "nginx:c", "nginx:z"}, false) 58 | }) 59 | 60 | t.Run("multiple-images-with-multiple-build-args", func(t *testing.T) { 61 | extractImages(t, filepath.Join("testdata", "Dockerfile.multistage.multiBuildArgs"), map[string]*string{"BASE_IMAGE": &baseImage, "REGISTRY_HOST": ®istryHost, "REGISTRY_PORT": ®istryPort, "NGINX_IMAGE": &nginxImage}, []string{"nginx:latest", "localhost:5000/nginx:latest", "scratch"}, false) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /client/client_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | dockerclient "github.com/moby/moby/client" 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/docker/go-sdk/client" 11 | ) 12 | 13 | func BenchmarkNew(b *testing.B) { 14 | b.Run("default", func(b *testing.B) { 15 | b.ResetTimer() 16 | for range b.N { 17 | cli, err := client.New(context.Background()) 18 | require.NoError(b, err) 19 | require.NoError(b, cli.Close()) 20 | } 21 | }) 22 | 23 | b.Run("with-host", func(b *testing.B) { 24 | opt := client.FromDockerOpt(dockerclient.WithHost("tcp://localhost:2375")) 25 | b.ResetTimer() 26 | for range b.N { 27 | cli, err := client.New(context.Background(), opt) 28 | require.NoError(b, err) 29 | require.NoError(b, cli.Close()) 30 | } 31 | }) 32 | 33 | b.Run("with-logger", func(b *testing.B) { 34 | opt := client.WithLogger(nil) // Using nil logger for benchmark 35 | b.ResetTimer() 36 | for range b.N { 37 | cli, err := client.New(context.Background(), opt) 38 | require.NoError(b, err) 39 | require.NoError(b, cli.Close()) 40 | } 41 | }) 42 | 43 | b.Run("with-healthcheck", func(b *testing.B) { 44 | noopHealthCheck := func(_ context.Context) func(c client.SDKClient) error { 45 | return func(_ client.SDKClient) error { 46 | return nil 47 | } 48 | } 49 | opt := client.WithHealthCheck(noopHealthCheck) 50 | b.ResetTimer() 51 | for range b.N { 52 | cli, err := client.New(context.Background(), opt) 53 | require.NoError(b, err) 54 | require.NoError(b, cli.Close()) 55 | } 56 | }) 57 | } 58 | 59 | func BenchmarkClientConcurrentCreation(b *testing.B) { 60 | b.Run("parallel-creation", func(b *testing.B) { 61 | b.ResetTimer() 62 | b.RunParallel(func(pb *testing.PB) { 63 | for pb.Next() { 64 | cli, err := client.New(context.Background()) 65 | require.NoError(b, err) 66 | require.NoError(b, cli.Close()) 67 | } 68 | }) 69 | }) 70 | 71 | b.Run("shared-client", func(b *testing.B) { 72 | cli, err := client.New(context.Background()) 73 | require.NoError(b, err) 74 | defer cli.Close() 75 | 76 | b.ResetTimer() 77 | b.RunParallel(func(pb *testing.PB) { 78 | for pb.Next() { 79 | // Just access the client to test concurrent access 80 | _ = cli.ClientVersion() 81 | } 82 | }) 83 | }) 84 | } 85 | 86 | func BenchmarkClientClose(b *testing.B) { 87 | b.Run("sequential-close", func(b *testing.B) { 88 | cli, err := client.New(context.Background()) 89 | require.NoError(b, err) 90 | b.ResetTimer() 91 | for range b.N { 92 | require.NoError(b, cli.Close()) 93 | } 94 | }) 95 | 96 | b.Run("concurrent-close", func(b *testing.B) { 97 | cli, err := client.New(context.Background()) 98 | require.NoError(b, err) 99 | b.ResetTimer() 100 | b.RunParallel(func(pb *testing.PB) { 101 | for pb.Next() { 102 | require.NoError(b, cli.Close()) 103 | } 104 | }) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /container/wait/testdata/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "strings" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | func run() error { 20 | mux := http.NewServeMux() 21 | mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 22 | w.WriteHeader(http.StatusOK) 23 | }) 24 | 25 | mux.HandleFunc("/auth-ping", func(w http.ResponseWriter, req *http.Request) { 26 | auth := req.Header.Get("Authorization") 27 | if strings.HasPrefix(auth, "Basic ") { 28 | up, err := base64.StdEncoding.DecodeString(auth[6:]) 29 | if err != nil { 30 | w.WriteHeader(http.StatusUnauthorized) 31 | return 32 | } 33 | if string(up) != "admin:admin" { 34 | w.WriteHeader(http.StatusUnauthorized) 35 | return 36 | } 37 | data, _ := io.ReadAll(req.Body) 38 | if bytes.Equal(data, []byte("ping")) { 39 | w.WriteHeader(http.StatusOK) 40 | _, _ = w.Write([]byte("pong")) 41 | return 42 | } else { 43 | w.WriteHeader(http.StatusBadRequest) 44 | return 45 | } 46 | } 47 | w.WriteHeader(http.StatusUnauthorized) 48 | }) 49 | 50 | mux.HandleFunc("/query-params-ping", func(w http.ResponseWriter, req *http.Request) { 51 | v := req.URL.Query().Get("v") 52 | if v == "" { 53 | w.WriteHeader(http.StatusBadRequest) 54 | return 55 | } 56 | 57 | w.WriteHeader(http.StatusOK) 58 | _, _ = w.Write([]byte("pong")) 59 | }) 60 | 61 | mux.HandleFunc("/headers", func(w http.ResponseWriter, req *http.Request) { 62 | h := req.Header.Get("X-request-header") 63 | if h != "" { 64 | w.Header().Add("X-response-header", h) 65 | w.WriteHeader(http.StatusOK) 66 | _, _ = w.Write([]byte("headers")) 67 | } else { 68 | w.WriteHeader(http.StatusBadRequest) 69 | } 70 | }) 71 | 72 | mux.HandleFunc("/ping", func(w http.ResponseWriter, req *http.Request) { 73 | data, _ := io.ReadAll(req.Body) 74 | if bytes.Equal(data, []byte("ping")) { 75 | w.WriteHeader(http.StatusOK) 76 | _, _ = w.Write([]byte("pong")) 77 | } else { 78 | w.WriteHeader(http.StatusBadRequest) 79 | } 80 | }) 81 | 82 | server := http.Server{Addr: ":6443", Handler: mux} 83 | go func() { 84 | log.Println("serving...") 85 | if err := server.ListenAndServeTLS("tls.pem", "tls-key.pem"); err != nil && !errors.Is(err, http.ErrServerClosed) { 86 | log.Println(err) 87 | } 88 | }() 89 | 90 | stopsig := make(chan os.Signal, 1) 91 | signal.Notify(stopsig, syscall.SIGINT, syscall.SIGTERM) 92 | <-stopsig 93 | 94 | log.Println("stopping...") 95 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 96 | defer cancel() 97 | if err := server.Shutdown(ctx); err != nil { 98 | return fmt.Errorf("shutdown: %w", err) 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func main() { 105 | if err := run(); err != nil { 106 | log.Fatal(err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.github/scripts/check-pre-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ============================================================================= 4 | # Pre-Release Check Script 5 | # ============================================================================= 6 | # Description: Verifies that pre-release was completed successfully for a module 7 | # by checking if the next-tag file exists and matches version.go 8 | # 9 | # Usage: ./.github/scripts/check-pre-release.sh 10 | # 11 | # Arguments: 12 | # module - Name of the module to check (required) 13 | # Examples: client, container, config, context, image, network 14 | # 15 | # Exit Codes: 16 | # 0 - Check passed 17 | # 1 - Check failed (missing files or version mismatch) 18 | # 19 | # Examples: 20 | # ./.github/scripts/check-pre-release.sh client 21 | # ./.github/scripts/check-pre-release.sh container 22 | # 23 | # Dependencies: 24 | # - grep (for parsing version.go) 25 | # 26 | # Files Checked: 27 | # - .github/scripts/.build/-next-tag - Pre-release version file 28 | # - /version.go - Current version file 29 | # 30 | # ============================================================================= 31 | 32 | set -e 33 | 34 | # Source common functions 35 | readonly SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 36 | source "${SCRIPT_DIR}/common.sh" 37 | 38 | # Get module name from argument and lowercase it 39 | readonly MODULE=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') 40 | 41 | if [[ -z "$MODULE" ]]; then 42 | echo "Error: Module name is required" 43 | echo "Usage: $0 " 44 | echo "Example: $0 client" 45 | exit 1 46 | fi 47 | 48 | echo "Checking if pre-release was completed for module: ${MODULE}" 49 | 50 | # Check if next-tag file exists 51 | readonly BUILD_FILE="${BUILD_DIR}/${MODULE}-next-tag" 52 | if [[ ! -f "${BUILD_FILE}" ]]; then 53 | echo "Error: Missing build file for module '${MODULE}' at ${BUILD_FILE}" 54 | echo "Please run 'make pre-release-all' or 'make pre-release' first (with DRY_RUN=false)" 55 | exit 1 56 | fi 57 | 58 | # Read next version from build file 59 | readonly NEXT_VERSION=$(cat "${BUILD_FILE}" | tr -d '\n') 60 | readonly NEXT_VERSION_NO_V="${NEXT_VERSION#v}" 61 | 62 | # Check if version.go exists 63 | readonly VERSION_FILE="${ROOT_DIR}/${MODULE}/version.go" 64 | if [[ ! -f "${VERSION_FILE}" ]]; then 65 | echo "Error: version.go not found at ${VERSION_FILE}" 66 | exit 1 67 | fi 68 | 69 | # Read current version from version.go 70 | readonly CURRENT_VERSION=$(get_version_from_file "${VERSION_FILE}") 71 | 72 | # Compare versions 73 | if [[ "${CURRENT_VERSION}" != "${NEXT_VERSION_NO_V}" ]]; then 74 | echo "Error: Version mismatch for module '${MODULE}'" 75 | echo " Expected (from ${BUILD_FILE}): ${NEXT_VERSION_NO_V}" 76 | echo " Actual (from ${VERSION_FILE}): ${CURRENT_VERSION}" 77 | echo "Please run 'make pre-release-all' or 'make pre-release' again (with DRY_RUN=false)" 78 | exit 1 79 | fi 80 | 81 | echo "✅ Pre-release check passed for module: ${MODULE} (version: ${NEXT_VERSION_NO_V})" 82 | exit 0 83 | -------------------------------------------------------------------------------- /image/pull_test.go: -------------------------------------------------------------------------------- 1 | package image_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io" 8 | "testing" 9 | 10 | dockerclient "github.com/moby/moby/client" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/docker/go-sdk/client" 14 | "github.com/docker/go-sdk/image" 15 | ) 16 | 17 | var noopShowProgress = func(_ io.ReadCloser) error { 18 | return nil 19 | } 20 | 21 | func TestPull(t *testing.T) { 22 | pull := func(t *testing.T, dockerClient client.SDKClient, expectedErr error, opts ...image.PullOption) { 23 | t.Helper() 24 | 25 | opts = append(opts, image.WithPullClient(dockerClient)) 26 | opts = append(opts, image.WithPullOptions(dockerclient.ImagePullOptions{})) 27 | 28 | ctx := context.Background() 29 | 30 | err := image.Pull(ctx, "nginx:alpine", opts...) 31 | if expectedErr != nil { 32 | require.ErrorContains(t, err, expectedErr.Error()) 33 | } else { 34 | require.NoError(t, err) 35 | } 36 | } 37 | 38 | t.Run("new-client", func(t *testing.T) { 39 | dockerClient, err := client.New(context.Background()) 40 | require.NoError(t, err) 41 | defer dockerClient.Close() 42 | 43 | pull(t, dockerClient, nil) 44 | }) 45 | 46 | t.Run("pull-handler/nil", func(t *testing.T) { 47 | cli, err := client.New(context.Background()) 48 | require.NoError(t, err) 49 | defer cli.Close() 50 | 51 | pull(t, cli, errors.New("pull handler is nil"), image.WithPullHandler(nil)) 52 | }) 53 | 54 | t.Run("pull-handler/noop", func(t *testing.T) { 55 | cli, err := client.New(context.Background()) 56 | require.NoError(t, err) 57 | defer cli.Close() 58 | 59 | pull(t, cli, nil, image.WithPullHandler(noopShowProgress)) 60 | }) 61 | 62 | t.Run("pull-handler/custom", func(t *testing.T) { 63 | cli, err := client.New(context.Background()) 64 | require.NoError(t, err) 65 | require.NotNil(t, cli) 66 | 67 | buf := &bytes.Buffer{} 68 | 69 | pull(t, cli, nil, image.WithPullHandler(func(r io.ReadCloser) error { 70 | _, err := io.Copy(buf, r) 71 | defer func() { 72 | if err := r.Close(); err != nil { 73 | t.Logf("failed to close reader: %v", err) 74 | } 75 | }() 76 | return err 77 | })) 78 | 79 | require.Contains(t, buf.String(), "Pulling from library/nginx") 80 | }) 81 | 82 | t.Run("with-credentials-fn/success", func(t *testing.T) { 83 | cli, err := client.New(context.Background()) 84 | require.NoError(t, err) 85 | defer cli.Close() 86 | 87 | pull(t, cli, nil, image.WithCredentialsFn(func(_ string) (string, string, error) { 88 | // no credentials because the image is public 89 | return "", "", nil 90 | })) 91 | }) 92 | 93 | t.Run("with-credentials-fn/error", func(t *testing.T) { 94 | cli, err := client.New(context.Background()) 95 | require.NoError(t, err) 96 | defer cli.Close() 97 | 98 | expectedErr := errors.New("test error") 99 | 100 | pull(t, cli, expectedErr, image.WithCredentialsFn(func(_ string) (string, string, error) { 101 | return "test", "test", expectedErr 102 | })) 103 | }) 104 | } 105 | --------------------------------------------------------------------------------