Skip to content

fix(dockerclient): version API requests when no API version was configured - #198

Open
Eljees wants to merge 2 commits into
mintoolkit:masterfrom
Eljees:fix/95-version-the-docker-api-requests
Open

Eljees wants to merge 2 commits into
mintoolkit:masterfrom
Eljees:fix/95-version-the-docker-api-requests

Conversation

@Eljees

@Eljees Eljees commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #95

The defect

dockerclient.New() builds its client with docker.NewVersionedClient(host, config.APIVersion). go-dockerclient only parses that string into the client's internal requestedAPIVersion when it is non-empty, and getURL() consults that field alone when it builds a request path. So with DOCKER_API_VERSION unset — the default for a plain mint build — every request the client sends goes out without a /vX.Y/ segment.

That is invisible against a daemon on a local socket. It is not invisible against a daemon reached through a proxy, which is what dind gives a CI pipeline: an unversioned request is read as coming from the oldest client the daemon supports, and the run dies with client version ... is too old. Minimum supported API version is ....

The client's own version negotiation does not rescue this. Its result is kept in expectedAPIVersion and never copied back into requestedAPIVersion, so the path stays unversioned. Setting DOCKER_API_VERSION by hand is the workaround the thread arrived at precisely because it is the only thing that populates the field.

The change

newVersionedClient wraps the construction: when the caller supplied a version, behaviour is exactly as before. When the caller did not, the daemon is asked once for its API version and the client is rebuilt with it, so requestedAPIVersion ends up set as if the caller had configured it by hand.

Two things it deliberately does not do:

  • If the probe fails, the original client is handed back rather than an error, so a caller with an unreachable daemon still sees the connection error it would always have seen instead of one manufactured here.
  • The rebuilt client gets SkipServerVersionCheck = true. Without it, go-dockerclient's lazy self-check would issue its own /version probe — and that probe now goes through getURL() too, so it would ask for /v1.44/version instead of /version. This is the same flag the existing config.APIVersion != "" branches already set.

Applied at the four call sites that construct a client from a host or a socket.

What I ran

Go 1.26.6, go test ./pkg/crt/docker/dockerclient/...:

  • control, client.go as it is on master: FAILthe /info request path "/info" carries no API version segment
  • with the change: ok

The test stands in a fake daemon with httptest.Server, answers the version probe truthfully, and asserts that the next request — Info()'s GET /info, the one a proxying daemon actually rejects — carries a version segment. No real Docker daemon needed.

go vet ./pkg/crt/... over the whole tree could not complete in my container: containers/storage needs gpgme and the btrfs headers, which are not installed there. That is an environment limit rather than anything about this change; CI covers it.

…gured

go-dockerclient only parses an API version into the client`s internal
requestedAPIVersion when the caller passes one, and getURL() consults that
field alone when building a request path. With DOCKER_API_VERSION unset - the
default for a plain "mint build" - every request therefore goes out without a
"/vX.Y/" segment. A daemon reached through a proxy, which is what dind gives a
CI pipeline, reads an unversioned request as coming from the oldest client it
supports and rejects it with "client version ... is too old". Setting
DOCKER_API_VERSION by hand is the only workaround the issue thread found, and
that is precisely because it is the only way the field gets populated.

Probe the daemon once and rebuild the client with the version it reports, so
requestedAPIVersion ends up set exactly as if the caller had configured it. If
the probe fails, hand back the original client so the caller still sees the
real connection error.

Fixes mintoolkit#95

Signed-off-by: Eljees <3.14hell@gmail.com>
@Eljees

Eljees commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

A note on the red checks, so they are not read as coming from this change: build, test and test (1.21) are failing on master itself at the same time — same three jobs, same make targets (Makefile:12 build, Makefile:35 test, Makefile:18 build_dev). This branch is a one-commit change on top of that base and does not touch the build.

What I could run in isolation was green: go test ./pkg/crt/docker/dockerclient/... fails on master's client.go with the /info request path "/info" carries no API version segment and passes with the change.

Happy to rebase once the base build is sorted out, if that helps.

@kcq

kcq commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

looking

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Several client construction paths remain unversioned, and the regression test needs isolation and stricter path matching.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity

Open (2)
What changed in this PR

Fixes unversioned Docker API requests when no API version is configured, particularly for proxy-backed daemons.

Changes:

  • Probes the daemon API version and rebuilds clients with it.
  • Updates four host/socket construction paths.
  • Adds regression coverage for versioned requests.
File Summary
pkg/​crt/​docker/​dockerclient/​client.go Moderate issues: TLS and DOCKER_HOST construction paths still bypass version discovery, leaving requests unversioned. Votes: 2 and 1.
pkg/​crt/​docker/​dockerclient/​client_api_version_test.go Moderate issues: the test should restore DOCKER_HOST and match the exact unversioned /version path. Votes: 2 and 1.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/crt/docker/dockerclient/client.go Outdated
Comment thread pkg/crt/docker/dockerclient/client_api_version_test.go
The first commit covered only the plain-host and socket paths. The TLS
branches build their clients through NewVersionedTLSClientFromBytes and the
DOCKER_HOST branch through NewClientFromEnv; both leave requestedAPIVersion
unset when no version is configured, so a TLS or environment-configured
daemon behind a proxy hits the same unversioned-request rejection.

Version discovery now lives in withDiscoveredAPIVersion, which takes the
constructor to rebuild with. Each path hands over its own, so the rebuilt
client keeps that path's transport, certificates and endpoint:

  - plain host and socket paths: docker.NewVersionedClient
  - TLS paths: the file's own newTLSClient closure
  - DOCKER_HOST path: docker.NewVersionedClientFromEnv

Tests:

  - the regression test no longer leaks DOCKER_HOST into the process
    environment (t.Setenv puts the original back) and matches the
    unversioned "/version" probe exactly, so a later "/v1.44/version"
    cannot be taken for it
  - a second end-to-end test covers the DOCKER_HOST path
  - four unit tests cover the shared discovery logic, including that the
    caller's own constructor receives the discovered version, which is
    what the TLS paths rely on

go test ./pkg/crt/docker/dockerclient/... gives 6 green tests with this
change. With master's client.go in place, both end-to-end tests fail on
`the /info request path "/info" carries no API version segment`.

Signed-off-by: Eljees <57435526+Eljees@users.noreply.github.com>
@Eljees

Eljees commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Both points taken; the first one changed the shape of the fix. Pushed as a follow-up commit rather than a force push, so the first round stays readable.

TLS and DOCKER_HOST paths. The helper did only wrap NewVersionedClient, as you say. Discovery now lives in withDiscoveredAPIVersion, which takes the constructor to rebuild with, so each path hands over its own and the rebuilt client keeps that path's transport, certificates and endpoint:

  • host and socket paths: docker.NewVersionedClient
  • the three TLS branches: this file's own newTLSClient closure, so cert/key/ca and the verify flag are re-read exactly as on the first construction
  • the DOCKER_HOST branch: docker.NewVersionedClientFromEnv. NewClientFromEnv reads DOCKER_API_VERSION itself, so that same string is what decides there whether a version was configured at all.

The test's environment. t.Setenv now holds DOCKER_HOST and DOCKER_API_VERSION before New runs, so whatever New writes into the process environment is put back when the test ends. The probe is matched as r.URL.Path == "/version" instead of a suffix, so a later /v1.44/version cannot be taken for it.

Coverage. A second end-to-end test drives the DOCKER_HOST path. Four unit tests cover the shared discovery: an explicit version is left alone, an unreachable daemon leaves the original client in place, a failing constructor does too, and the caller's own constructor receives the discovered version. That last one is what the TLS branches rest on, since they differ from the plain path only in which constructor they pass.

go test ./pkg/crt/docker/dockerclient/...   -> 6 tests, all green

With master's client.go in place and the unit-test file removed, both end-to-end tests fail:

the /info request path "/info" carries no API version segment although the daemon answered the version probe (#95)
the /info request path "/info" carries no API version segment on the DOCKER_HOST path (#95)

One correction to my own comment from the first round: it claimed the lazy self-check had to be skipped because the bootstrap probe would ask a daemon for /v1.44/version and fail to parse the result. A real daemon serves that path perfectly well. The honest reason is narrower and that is what the comment says now: the version was just read from this daemon, so repeating the probe buys nothing, which is also what happens today when someone sets DOCKER_API_VERSION by hand.

The three red jobs are still the ones failing on master itself, untouched by this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mint build fails when running in Bitbucket pipelines

3 participants