diff --git a/.agents/skills/adversarial-review/SKILL.md b/.agents/skills/adversarial-review/SKILL.md index 4270c0f..9b75dd8 100644 --- a/.agents/skills/adversarial-review/SKILL.md +++ b/.agents/skills/adversarial-review/SKILL.md @@ -104,7 +104,9 @@ These are the ways this codebase breaks that generic reviewers miss. Read - **Conventional Commits with a scope** (`()[!]: `), enforced by the `conventional-pre-commit` hook with `--force-scope`. Release notes are generated from them. - **depguard deny list** in `.golangci.yaml`: `github.com/sirupsen/logrus` outside - `internal/logger`, `github.com/pkg/errors`, and the `github.com/instana/testify` fork. A new + `internal/logger`, `github.com/pkg/errors`, the `github.com/instana/testify` fork, and + `github.com/docker/docker/…` (vulnerable in every version — the Docker SDK is + `github.com/moby/moby/client` + `github.com/moby/moby/api`). A new import of any of them — or a `//nolint:depguard` to sneak one in — is a finding. ## 4. Adversarial passes — language-agnostic diff --git a/.agents/skills/go-style-guide/SKILL.md b/.agents/skills/go-style-guide/SKILL.md index eaf3b9e..bd4d695 100644 --- a/.agents/skills/go-style-guide/SKILL.md +++ b/.agents/skills/go-style-guide/SKILL.md @@ -29,7 +29,7 @@ import ( "fmt" // Group 2: third-party (everything that is NOT this module) - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sys/unix" "gopkg.in/yaml.v3" @@ -212,6 +212,7 @@ tags. | `github.com/sirupsen/logrus` | `github.com/leinardi/swarm-device-access/internal/logger` → `logger.L()` | | `github.com/pkg/errors` | stdlib `errors` + `fmt.Errorf(...%w...)` | | `github.com/instana/testify` | `github.com/stretchr/testify` | +| `github.com/docker/docker/…` | `github.com/moby/moby/client` + `github.com/moby/moby/api` (GO-2026-4887, GO-2026-4883) | --- diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cfb2cde..70ae8f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,6 +54,26 @@ jobs: golangci-lint-fmt golangci-lint-full + # A dedicated job rather than an entry in the pre-commit-hooks list above: reviewdog runs hooks + # only on the files a PR changes, and the audit-deps-go hook only fires on go.mod/go.sum, so new + # code reaching an already-vulnerable API, or a transitively reintroduced banned module, would + # never be scanned there. + audit-deps: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: true + + - name: Audit Go dependencies + run: make audit-deps + markdownlint: runs-on: ubuntu-latest diff --git a/.golangci.yaml b/.golangci.yaml index 8d2637a..749e4c9 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -37,6 +37,13 @@ linters: desc: Should be replaced by standard lib errors package. - pkg: "github.com/instana/testify" desc: It's a fork of github.com/stretchr/testify. + docker-sdk: + deny: + # Trailing slash: matches github.com/docker/docker/... only, not github.com/docker/docker-credential-helpers. + - pkg: "github.com/docker/docker/" + desc: >- + Affected by GO-2026-4887 and GO-2026-4883 in every version, with no fix in this module. + Use github.com/moby/moby/client and github.com/moby/moby/api instead. dupl: threshold: 100 funlen: diff --git a/.mk/audit-deps.mk b/.mk/audit-deps.mk new file mode 100644 index 0000000..c12d88d --- /dev/null +++ b/.mk/audit-deps.mk @@ -0,0 +1,54 @@ +ifndef MK_LOCAL_AUDIT_DEPS_INCLUDED +MK_LOCAL_AUDIT_DEPS_INCLUDED := 1 + +# Local snippet (NOT part of make-common): check the Go dependencies this repository ships against +# known vulnerabilities, and keep banned modules out of the module graph. +# +# What this is and is not. govulncheck is source-aware: it reports a vulnerable module only when +# the build actually reaches the affected symbol, so it answers "is this repository exposed", not +# "does a vulnerable version appear in the graph". The banned-module check answers the graph +# question for the modules listed in BANNED_GO_MODULES: depguard (.golangci.yaml) only sees this +# repository's own import lines, so it cannot notice a banned module coming back transitively +# through some other dependency. go mod why -m can. +# +# Both need the network, and govulncheck over every package is too slow to run on every Go +# change. The pre-commit hook runs this same target, but only when go.mod or go.sum change; CI runs +# it on every pull request (the audit-deps job in .github/workflows/ci.yaml). +# +# The version is pinned so two runs mean the same thing. Bump it deliberately. + +GOVULNCHECK_VERSION ?= v1.8.0 +GOVULNCHECK ?= golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) + +# github.com/docker/docker is affected by GO-2026-4887 and GO-2026-4883 in every version, with no +# fix in that module; the SDK is github.com/moby/moby/client + github.com/moby/moby/api. +BANNED_GO_MODULES ?= github.com/docker/docker + +.PHONY: audit-deps +audit-deps: audit-deps-go audit-deps-banned ## Scan Go dependencies for known vulnerabilities and banned modules (network required) + +.PHONY: audit-deps-go +audit-deps-go: ## Run the pinned govulncheck over every Go package + $(GO) run $(GOVULNCHECK) ./... + +# go mod why -m prints "(main module does not need module )" when no package in the build +# (tests included) imports the module; anything else is the import chain that pulls it in. +.PHONY: audit-deps-banned +audit-deps-banned: ## Fail if a module in BANNED_GO_MODULES is reachable from the main module + @status=0; \ + for mod in $(BANNED_GO_MODULES); do \ + if ! why="$$($(GO) mod why -m "$$mod" 2>&1)"; then \ + echo "[audit-deps] go mod why -m $$mod failed:" >&2; \ + printf '%s\n' "$$why" >&2; \ + status=1; \ + elif printf '%s\n' "$$why" | grep -qxF "(main module does not need module $$mod)"; then \ + echo "[audit-deps] $$mod: not needed"; \ + else \ + echo "[audit-deps] banned module $$mod is needed by the main module, via:" >&2; \ + printf '%s\n' "$$why" >&2; \ + status=1; \ + fi; \ + done; \ + exit $$status + +endif # MK_LOCAL_AUDIT_DEPS_INCLUDED diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0cad11..f9f79c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,6 +65,21 @@ repos: additional_dependencies: ["prettier@3.7.4"] files: \.(yml|yaml)$ args: ["--write"] + # Dependency-change check, and nothing more. It runs the same target as `make audit-deps` -- + # the full source-aware govulncheck scan over every package plus the banned-module check -- + # but only when go.mod or go.sum change, which is what limits it: it catches a bad upgrade + # at the moment it is made, and nothing else. It does not catch a CVE disclosed against a + # dependency nobody touched since, and it never fires on a .go change, so new code reaching + # an already-vulnerable API goes unseen here. Widening the trigger is not the fix: it would + # put a multi-minute network scan on every Go commit. The audit-deps CI job runs the same + # target on every pull request. + - id: audit-deps-go + name: go dependency audit + entry: make audit-deps + language: system + pass_filenames: false + require_serial: true + files: ^go\.(mod|sum)$ - repo: https://github.com/adrienverge/yamllint rev: v1.38.0 hooks: diff --git a/.serena/memories/tech_stack.md b/.serena/memories/tech_stack.md index a98473b..ca50479 100644 --- a/.serena/memories/tech_stack.md +++ b/.serena/memories/tech_stack.md @@ -1,10 +1,10 @@ # Tech Stack -- **Language**: Go 1.26.3 (module `github.com/leinardi/swarm-device-access`) +- **Language**: Go 1.26.8 (module `github.com/leinardi/swarm-device-access`) - **Build**: Make wrapping `go`; shared `.mk/` snippets from `leinardi/make-common@v1` - **Key deps**: - `github.com/cilium/ebpf v0.19.0` — BPF program attach (cgroup v2) - - `github.com/docker/docker v28.5.2` — Docker API client + - `github.com/moby/moby/client v0.6.0` + `github.com/moby/moby/api v1.56.0` — Docker API client and types - `github.com/godbus/dbus/v5 v5.1.0` — systemd DBus watcher - `github.com/prometheus/client_golang v1.23.2` — metrics - `golang.org/x/sys v0.42.0` — unix syscalls diff --git a/AGENTS.md b/AGENTS.md index 6487f8c..4dad59f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ make go-build # cross-compiles to Linux into ./dist/ (works from macOS/Win) make go-test # CGO_ENABLED=1 go test -race ./... make go-vet make go-tidy # go mod tidy + go mod verify +make audit-deps # govulncheck + banned-module check (network required); also runs in CI and as a pre-commit hook on go.mod/go.sum changes make check # pre-commit on all files make check-stage # pre-commit on staging area only make docker-build diff --git a/Makefile b/Makefile index 03b0c96..c0d8252 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ MK_COMMON_DIR := $(REPO_ROOT)/.mk MK_COMMON_FILES := docker.mk help.mk go.mk password.mk pre-commit.mk # Repo-local snippets that are NOT in make-common -MK_LOCAL_FILES := docker-run.mk integration-test.mk +MK_LOCAL_FILES := docker-run.mk integration-test.mk audit-deps.mk MK_COMMON_BOOTSTRAP_SCRIPT := $(REPO_ROOT)/scripts/bootstrap-mk-common.sh diff --git a/README.md b/README.md index e0ceba6..ff4f46e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ The daemon needs host-level privileges: `privileged`, host cgroup namespace, host PID namespace, host user namespace, the host Docker socket, `/sys` mounted at `/host/sys`, and `/dev` mounted into the daemon container. +It requires Docker Engine 19.03 or newer (API 1.40): the Docker client it uses +refuses older engines. + Swarm does not allow those runtime options directly on a service. The common workaround is to deploy a small wrapper service that runs the Docker CLI and uses the host Docker socket to launch the real privileged daemon container. diff --git a/cmd/swarm-device-access/main.go b/cmd/swarm-device-access/main.go index d765216..a546a29 100644 --- a/cmd/swarm-device-access/main.go +++ b/cmd/swarm-device-access/main.go @@ -29,7 +29,7 @@ import ( "os/signal" "syscall" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/leinardi/swarm-device-access/internal/config" "github.com/leinardi/swarm-device-access/internal/daemon" @@ -98,10 +98,8 @@ func run() int { // SIGHUP: reload the config file and update hot settings + logger. go watchSIGHUP(rootCtx, store) - cli, err := client.NewClientWithOpts( - client.WithHost("unix://"+*dockerSocket), - client.WithAPIVersionNegotiation(), - ) + // API version negotiation is the client default; it runs lazily on the first request. + cli, err := client.New(client.WithHost("unix://" + *dockerSocket)) if err != nil { log.Error("docker client init failed", "err", err) @@ -113,7 +111,7 @@ func run() int { isSwarmManager := false - nodeInfo, infoErr := cli.Info(rootCtx) + infoResult, infoErr := cli.Info(rootCtx, client.InfoOptions{}) if infoErr != nil { log.Warn( "could not query docker info; assuming worker node (service-label inspection disabled)", @@ -121,10 +119,10 @@ func run() int { infoErr, ) } else { - isSwarmManager = nodeInfo.Swarm.ControlAvailable + isSwarmManager = infoResult.Info.Swarm.ControlAvailable log.Info("swarm role detected", "manager", isSwarmManager, - "local_node_state", nodeInfo.Swarm.LocalNodeState, + "local_node_state", infoResult.Info.Swarm.LocalNodeState, ) } diff --git a/docs/architecture.md b/docs/architecture.md index 91e54d3..f6d58fb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -271,3 +271,5 @@ Ensure the host DBus socket is bind-mounted as `-v /run/dbus/system_bus_socket:/ ### Daemon cannot connect to Docker Check the socket path with `-docker-socket`. Default: `/var/run/docker.sock`. On some hosts Docker Desktop uses a different path. + +The daemon requires Docker Engine 19.03+ (API 1.40). The `github.com/moby/moby/client` SDK refuses older engines with `API version … is not supported by this client: the minimum supported API version is 1.40`. diff --git a/go.mod b/go.mod index 7845209..eac3458 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/leinardi/swarm-device-access -go 1.26.3 +go 1.26.8 require ( github.com/cilium/ebpf v0.22.0 - github.com/docker/docker v28.5.2+incompatible github.com/godbus/dbus/v5 v5.2.2 + github.com/moby/moby/api v1.56.0 + github.com/moby/moby/client v0.6.0 github.com/prometheus/client_golang v1.24.1 golang.org/x/sys v0.48.0 gopkg.in/yaml.v3 v3.0.1 @@ -17,7 +18,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -25,23 +25,16 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/atomicwriter v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect - golang.org/x/time v0.15.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gotest.tools/v3 v3.5.2 // indirect ) diff --git a/go.sum b/go.sum index 280acd8..9e1ceed 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,7 @@ -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= @@ -14,14 +10,10 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -41,8 +33,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= @@ -61,22 +51,16 @@ github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= -github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= +github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= +github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= @@ -89,30 +73,22 @@ github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+ github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= @@ -123,16 +99,6 @@ golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -142,3 +108,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index b57c038..fa2c130 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -23,8 +23,7 @@ import ( "fmt" "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/events" + "github.com/moby/moby/client" "github.com/leinardi/swarm-device-access/internal/logger" "github.com/leinardi/swarm-device-access/internal/observability" @@ -35,8 +34,11 @@ import ( // dockerAPI is the subset of *client.Client used by the daemon loop. It exists // so tests can inject a fake event stream and container list. type dockerAPI interface { - ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) - Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) + ContainerList( + ctx context.Context, + options client.ContainerListOptions, + ) (client.ContainerListResult, error) + Events(ctx context.Context, options client.EventsListOptions) client.EventsResult } // Options bundles the dependencies required by Run. @@ -65,7 +67,7 @@ func run(ctx context.Context, opts Options, startWatcher func(context.Context, O // The client delivers messages on an unbuffered channel, so events that // arrive during the enumeration below wait (with backpressure on the // socket) until listenEvents starts consuming them. - msgs, errs := opts.Docker.Events(ctx, eventListOptions(formatSince(since))) + stream := opts.Docker.Events(ctx, eventListOptions(formatSince(since))) processed := make(map[string]time.Time) @@ -82,7 +84,7 @@ func run(ctx context.Context, opts Options, startWatcher func(context.Context, O startWatcher(ctx, opts) - listenEvents(ctx, opts, processed, since, msgs, errs) + listenEvents(ctx, opts, processed, since, stream.Messages, stream.Err) return nil } @@ -100,11 +102,13 @@ func processExistingContainers( ) error { log := logger.L() - containers, err := cli.ContainerList(ctx, container.ListOptions{}) + list, err := cli.ContainerList(ctx, client.ContainerListOptions{}) if err != nil { return fmt.Errorf("list containers: %w", err) } + containers := list.Items + log.Debug("enumerating running containers", "count", len(containers)) for idx := range containers { diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go index 9d45e74..b964ad9 100644 --- a/internal/daemon/daemon_test.go +++ b/internal/daemon/daemon_test.go @@ -29,9 +29,9 @@ import ( "testing" "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" "github.com/prometheus/client_golang/prometheus" "github.com/leinardi/swarm-device-access/internal/config" @@ -56,8 +56,8 @@ type fakeDocker struct { func (f *fakeDocker) ContainerList( _ context.Context, - _ container.ListOptions, -) ([]container.Summary, error) { + _ client.ContainerListOptions, +) (client.ContainerListResult, error) { f.mu.Lock() f.calls = append(f.calls, "list") f.listTime = time.Now() @@ -67,13 +67,13 @@ func (f *fakeDocker) ContainerList( f.onList() } - return f.containers, nil + return client.ContainerListResult{Items: f.containers}, nil } func (f *fakeDocker) Events( _ context.Context, - options events.ListOptions, -) (msgs <-chan events.Message, errs <-chan error) { + options client.EventsListOptions, +) client.EventsResult { f.mu.Lock() defer f.mu.Unlock() @@ -81,10 +81,10 @@ func (f *fakeDocker) Events( f.sinces = append(f.sinces, options.Since) if len(f.sinces) == 1 && f.firstMsgs != nil { - return f.firstMsgs, f.firstErrs + return client.EventsResult{Messages: f.firstMsgs, Err: f.firstErrs} } - return make(chan events.Message), make(chan error) + return client.EventsResult{Messages: make(chan events.Message), Err: make(chan error)} } func (f *fakeDocker) snapshot() (calls, sinces []string, listTime time.Time) { @@ -105,22 +105,23 @@ type recordingInspector struct { func (r *recordingInspector) ContainerInspect( _ context.Context, containerID string, -) (container.InspectResponse, error) { + _ client.ContainerInspectOptions, +) (client.ContainerInspectResult, error) { r.mu.Lock() r.ids = append(r.ids, containerID) r.mu.Unlock() - return container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: nil}, + return client.ContainerInspectResult{ + Container: container.InspectResponse{State: nil}, }, nil } -func (*recordingInspector) ServiceInspectWithRaw( +func (*recordingInspector) ServiceInspect( _ context.Context, _ string, - _ swarm.ServiceInspectOptions, -) (swarm.Service, []byte, error) { - return swarm.Service{}, nil, nil + _ client.ServiceInspectOptions, +) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{}, nil } func (r *recordingInspector) inspected() []string { diff --git a/internal/daemon/events.go b/internal/daemon/events.go index 47f44ad..18c65d6 100644 --- a/internal/daemon/events.go +++ b/internal/daemon/events.go @@ -23,8 +23,8 @@ import ( "errors" "time" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" "github.com/leinardi/swarm-device-access/internal/logger" "github.com/leinardi/swarm-device-access/internal/observability" @@ -41,17 +41,15 @@ type applyFn func(ctx context.Context, id string) error // eventListOptions returns the Docker event subscription options: "start" // and "unpause" container events at or after since. -func eventListOptions(since string) events.ListOptions { - return events.ListOptions{ +func eventListOptions(since string) client.EventsListOptions { + return client.EventsListOptions{ Since: since, - Filters: filters.NewArgs( - filters.Arg("event", "start"), - filters.Arg("event", "unpause"), - ), + // make, not the zero value: a nil client.Filters panics on Add. + Filters: make(client.Filters).Add("event", "start", "unpause"), } } -// formatSince formats a timestamp for events.ListOptions.Since. +// formatSince formats a timestamp for client.EventsListOptions.Since. func formatSince(since time.Time) string { return since.UTC().Format(time.RFC3339Nano) } @@ -102,10 +100,11 @@ func listenEvents( } if msgs == nil { - msgs, errs = opts.Docker.Events( + stream := opts.Docker.Events( ctx, eventListOptions(resubscribeSince(since, lastEventNano)), ) + msgs, errs = stream.Messages, stream.Err } observability.SetReady(true) @@ -167,6 +166,13 @@ func consumeEvents( continue } + // Checked before the error itself: once ctx is canceled the client + // can end the stream with a closed-body read error rather than + // context.Canceled, and that is a shutdown, not a stream failure. + if ctx.Err() != nil { + return false + } + if errors.Is(streamErr, context.Canceled) || errors.Is(streamErr, context.DeadlineExceeded) { return false diff --git a/internal/daemon/events_test.go b/internal/daemon/events_test.go index b56a411..85ffbf4 100644 --- a/internal/daemon/events_test.go +++ b/internal/daemon/events_test.go @@ -21,11 +21,14 @@ package daemon import ( "context" "errors" + "maps" + "strings" "sync/atomic" "testing" "time" - "github.com/docker/docker/api/types/events" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" ) var errTransportEOF = errors.New("transport EOF") @@ -161,6 +164,75 @@ func TestConsumeEvents_ContextErrFromStreamErrorNoReconnect(t *testing.T) { } } +// cancelledCtx reports itself canceled through Err but never closes Done, so a +// select in consumeEvents can only take the errs case. That pins the race where +// the stream error and the cancellation arrive together and select picks errs. +type cancelledCtx struct{} + +func (cancelledCtx) Deadline() (time.Time, bool) { return time.Time{}, false } + +func (cancelledCtx) Done() <-chan struct{} { return nil } + +func (cancelledCtx) Err() error { return context.Canceled } + +func (cancelledCtx) Value(any) any { return nil } + +func TestConsumeEvents_ArbitraryStreamErrorAfterCancelNoReconnect(t *testing.T) { + logs := captureLogs(t) + + ctx := cancelledCtx{} + msgs, errs := makeChans(0, 1) + backoff := minBackoff + + // A canceled stream can end with a closed-body read error rather than + // context.Canceled; it must be treated as shutdown, not a stream failure. + errs <- errTransportEOF + + got := consumeEvents( + ctx, + msgs, + errs, + map[string]time.Time{}, + &backoff, + nil, + new(int64), + nil, + noopApply, + ) + if got { + t.Error("consumeEvents should return false when the context is already canceled") + } + + if backoff != minBackoff { + t.Errorf( + "backoff = %v, want %v: a shutdown must not count as a failure", + backoff, + minBackoff, + ) + } + + if strings.Contains(logs.String(), "docker events stream error") { + t.Errorf("unexpected stream-error log at shutdown:\n%s", logs.String()) + } +} + +func TestEventListOptions(t *testing.T) { + t.Parallel() + + const since = "2026-01-02T03:04:05.000000006Z" + + got := eventListOptions(since) + + if got.Since != since { + t.Errorf("Since = %q, want %q", got.Since, since) + } + + want := client.Filters{"event": {"start": true, "unpause": true}} + if !maps.EqualFunc(got.Filters, want, maps.Equal) { + t.Errorf("Filters = %v, want %v", got.Filters, want) + } +} + func TestConsumeEvents_ChannelCloseReturnsReconnect(t *testing.T) { ctx := context.Background() msgs, errs := makeChans(0, 0) diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 042bcd9..70eabb9 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -23,8 +23,9 @@ import ( "fmt" "path/filepath" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/leinardi/swarm-device-access/internal/cgroup" "github.com/leinardi/swarm-device-access/internal/config" @@ -39,12 +40,16 @@ const swarmServiceIDLabel = "com.docker.swarm.service.id" // It exists solely to allow unit tests to inject a fake without standing up a // real Docker daemon. type DockerInspector interface { - ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) - ServiceInspectWithRaw( + ContainerInspect( + ctx context.Context, + containerID string, + options client.ContainerInspectOptions, + ) (client.ContainerInspectResult, error) + ServiceInspect( ctx context.Context, serviceID string, - opts swarm.ServiceInspectOptions, - ) (swarm.Service, []byte, error) + options client.ServiceInspectOptions, + ) (client.ServiceInspectResult, error) } // deviceRuleKey is the deduplication key for cgroup device rules collected @@ -76,11 +81,17 @@ type Processor struct { func (p *Processor) ProcessContainer(ctx context.Context, containerID string) error { log := logger.L() - info, inspectErr := p.Inspector.ContainerInspect(ctx, containerID) + inspected, inspectErr := p.Inspector.ContainerInspect( + ctx, + containerID, + client.ContainerInspectOptions{}, + ) if inspectErr != nil { return fmt.Errorf("inspect container %q: %w", containerID, inspectErr) } + info := inspected.Container + if info.State == nil || info.State.Pid == 0 { log.Debug("container has no live pid; skipping", "id", containerID) p.Metrics.RecordContainerSkipped("no_pid") @@ -263,12 +274,10 @@ func (p *Processor) resolveServiceLabels( return svc, nil } - var svcErr error - - svc, _, svcErr = p.Inspector.ServiceInspectWithRaw( + inspected, svcErr := p.Inspector.ServiceInspect( ctx, serviceID, - swarm.ServiceInspectOptions{}, + client.ServiceInspectOptions{}, ) if svcErr != nil { log.Warn("could not inspect parent service; using container labels only", @@ -280,6 +289,7 @@ func (p *Processor) resolveServiceLabels( return swarm.Service{}, nil } + svc = inspected.Service serviceLabels = svc.Spec.Labels for _, unknownKey := range policy.UnknownLabels(serviceLabels) { diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 98b9396..3ef5ddd 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -30,9 +30,10 @@ import ( "strings" "testing" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/leinardi/swarm-device-access/internal/cgroup" "github.com/leinardi/swarm-device-access/internal/config" @@ -52,18 +53,19 @@ type fakeInspector struct { func (f *fakeInspector) ContainerInspect( _ context.Context, _ string, -) (container.InspectResponse, error) { - return f.result, f.err + _ client.ContainerInspectOptions, +) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{Container: f.result}, f.err } -func (f *fakeInspector) ServiceInspectWithRaw( +func (f *fakeInspector) ServiceInspect( _ context.Context, _ string, - _ swarm.ServiceInspectOptions, -) (swarm.Service, []byte, error) { + _ client.ServiceInspectOptions, +) (client.ServiceInspectResult, error) { f.serviceCalls++ - return f.serviceResult, nil, f.serviceErr + return client.ServiceInspectResult{Service: f.serviceResult}, f.serviceErr } // buildProcRoot creates a minimal /proc//{cgroup,mountinfo} structure @@ -208,9 +210,7 @@ func TestProcessContainer_InspectError(t *testing.T) { func TestProcessContainer_NilState(t *testing.T) { insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{ - State: nil, - }, + State: nil, }} proc := &Processor{ Inspector: insp, @@ -228,7 +228,7 @@ func TestProcessContainer_NilState(t *testing.T) { func TestProcessContainer_ZeroPid(t *testing.T) { state := &container.State{Pid: 0} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, }} proc := &Processor{ Inspector: insp, @@ -253,7 +253,7 @@ func TestProcessContainer_NoDevMounts(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Mounts: []container.MountPoint{ {Source: "/tmp/data", Destination: "/data", Type: mount.TypeBind}, {Source: "/var/log", Destination: "/logs", Type: mount.TypeBind}, @@ -288,7 +288,7 @@ func TestProcessContainer_DevMountFilterApplied(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Mounts: []container.MountPoint{ {Source: "/tmp/data", Destination: "/data", Type: mount.TypeBind}, {Source: "/dev/null", Destination: "/dev/null", Type: mount.TypeBind}, @@ -319,7 +319,7 @@ func TestProcessContainer_DevMount_DryRunNoError(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Mounts: []container.MountPoint{ {Source: "/dev/null", Destination: "/dev/null", Type: mount.TypeBind}, }, @@ -347,7 +347,7 @@ func TestProcessContainer_DeduplicatesDuplicateMounts(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Mounts: []container.MountPoint{ {Source: "/dev/null", Destination: "/dev/null", Type: mount.TypeBind}, {Source: "/dev/null", Destination: "/dev/null2", Type: mount.TypeBind}, @@ -376,7 +376,7 @@ func TestProcessContainer_OptInSkipsUnlabelled(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Mounts: []container.MountPoint{ {Source: "/dev/null", Destination: "/dev/null", Type: mount.TypeBind}, }, @@ -404,7 +404,7 @@ func TestProcessContainer_OptInProcessesEnabled(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Config: &container.Config{ Labels: map[string]string{policy.LabelEnable: "true"}, }, @@ -798,7 +798,7 @@ func TestProcessContainer_DevDirectorySummary(t *testing.T) { state := &container.State{Pid: pid} insp := &fakeInspector{result: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{State: state}, + State: state, Mounts: []container.MountPoint{ {Source: "/dev", Destination: "/dev", Type: mount.TypeBind}, }, @@ -879,9 +879,7 @@ func TestProcessContainer_SwarmServiceLabels(t *testing.T) { maps.Copy(labels, extraLabels) return container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{Pid: pid}, - }, + State: &container.State{Pid: pid}, Config: &container.Config{Labels: labels}, } } @@ -937,9 +935,7 @@ func TestProcessContainer_SwarmServiceLabels(t *testing.T) { { name: "non-Swarm passthrough", containerInfo: container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{ - State: &container.State{Pid: pid}, - }, + State: &container.State{Pid: pid}, Config: &container.Config{Labels: map[string]string{ policy.LabelEnable: "true", }}, @@ -1029,11 +1025,11 @@ func TestProcessContainer_SwarmServiceLabels(t *testing.T) { logOutput := buf.String() if tc.wantServiceCall && inspector.serviceCalls == 0 { - t.Error("expected ServiceInspectWithRaw to be called, was not") + t.Error("expected ServiceInspect to be called, was not") } if !tc.wantServiceCall && inspector.serviceCalls > 0 { - t.Errorf("expected no ServiceInspectWithRaw call, got %d", inspector.serviceCalls) + t.Errorf("expected no ServiceInspect call, got %d", inspector.serviceCalls) } if tc.wantLogMsg != "" && !strings.Contains(logOutput, tc.wantLogMsg) { diff --git a/test/integration/daemon_test.go b/test/integration/daemon_test.go index bc901e6..66cf3f6 100644 --- a/test/integration/daemon_test.go +++ b/test/integration/daemon_test.go @@ -47,9 +47,8 @@ import ( "testing" "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - dockerclient "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + dockerclient "github.com/moby/moby/client" "github.com/leinardi/swarm-device-access/internal/policy" ) @@ -411,12 +410,12 @@ func TestDaemon_DryRun_UnpauseEvent(t *testing.T) { t.Fatal("timeout: start event not detected") } - err := cli.ContainerPause(ctx, containerID) + _, err := cli.ContainerPause(ctx, containerID, dockerclient.ContainerPauseOptions{}) if err != nil { t.Fatalf("pause container: %v", err) } - err = cli.ContainerUnpause(ctx, containerID) + _, err = cli.ContainerUnpause(ctx, containerID, dockerclient.ContainerUnpauseOptions{}) if err != nil { t.Fatalf("unpause container: %v", err) } @@ -596,10 +595,7 @@ func findBinary(ctx context.Context, t *testing.T) string { func requireDocker(t *testing.T) *dockerclient.Client { t.Helper() - cli, err := dockerclient.NewClientWithOpts( - dockerclient.FromEnv, - dockerclient.WithAPIVersionNegotiation(), - ) + cli, err := dockerclient.New(dockerclient.FromEnv) if err != nil { t.Skipf("Docker client init failed: %v", err) } @@ -607,7 +603,7 @@ func requireDocker(t *testing.T) *dockerclient.Client { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _, err = cli.Ping(ctx) + _, err = cli.Ping(ctx, dockerclient.PingOptions{}) if err != nil { t.Skipf("Docker daemon not reachable: %v", err) } @@ -624,7 +620,7 @@ func ensureTestImage(t *testing.T, cli *dockerclient.Client) { ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() - reader, err := cli.ImagePull(ctx, testImage, image.PullOptions{}) + reader, err := cli.ImagePull(ctx, testImage, dockerclient.ImagePullOptions{}) if err != nil { errPullTestImage = err @@ -700,24 +696,23 @@ func startTestContainer( ) string { t.Helper() - resp, err := cli.ContainerCreate(ctx, - &container.Config{ + resp, err := cli.ContainerCreate(ctx, dockerclient.ContainerCreateOptions{ + Config: &container.Config{ Image: testImage, Cmd: []string{"sh", "-c", "sleep 30"}, Labels: labels, }, - &container.HostConfig{ + HostConfig: &container.HostConfig{ Binds: binds, }, - nil, nil, "", - ) + }) if err != nil { t.Fatalf("create test container: %v", err) } - err = cli.ContainerStart(ctx, resp.ID, container.StartOptions{}) + _, err = cli.ContainerStart(ctx, resp.ID, dockerclient.ContainerStartOptions{}) if err != nil { - _ = cli.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}) + _, _ = cli.ContainerRemove(ctx, resp.ID, dockerclient.ContainerRemoveOptions{Force: true}) t.Fatalf("start test container: %v", err) } @@ -733,7 +728,7 @@ func removeContainer(t *testing.T, cli *dockerclient.Client, containerID string) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - _ = cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) + _, _ = cli.ContainerRemove(ctx, containerID, dockerclient.ContainerRemoveOptions{Force: true}) } // scanOutput reads lines from r and sends a struct{} on each channel in signals