Skip to content

cli/grpc: add gRPC connections to the Docker Engine's API endpoint - #7183

Open
ndeloof wants to merge 4 commits into
docker:masterfrom
ndeloof:cli-grpc
Open

cli/grpc: add gRPC connections to the Docker Engine's API endpoint#7183
ndeloof wants to merge 4 commits into
docker:masterfrom
ndeloof:cli-grpc

Conversation

@ndeloof

@ndeloof ndeloof commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

- What I did

Added a cli/grpc package providing a single way to establish a gRPC client connection to the daemon's API endpoint, for CLI plugins consuming the daemon's gRPC services (BuildKit's control API today, services published by daemon extensions — moby/moby#53021 — tomorrow).

Connect() (or the lower-level Dial()) hides the transport details of the current docker context:

  • native HTTP/2 for daemons with API ≥ v1.53: h2c over the API client's own dialer (unix/npipe sockets, plain tcp, ssh:// and other connection helpers), or an ALPN-negotiated TLS handshake for tcp:// endpoints with TLS material;
  • fallback to the deprecated POST /grpc HTTP/1.1 upgrade endpoint for older daemons, as buildx currently hand-rolls.

Also exports Endpoint.TLSConfig() in cli/context/docker (previously private), needed for the ALPN path.

google.golang.org/grpc moves from indirect to direct dependency.

- How I did it

Daemon API version is probed with a plain Ping; the connection is then established with grpc.NewClient using either a custom dialer (h2c, legacy upgrade) or gRPC's TLS credentials (ALPN). See the package documentation for details.

Note — capability signal in Ping
Native connections are probed with a health-check RPC before being returned, because the API version alone cannot be trusted: an intermediary may not relay HTTP/2 even when the daemon advertises native support (Docker Desktop's API proxy today — its engine reports API 1.55 while only the legacy upgrade path gets through). The probe costs one round-trip per Connect on the native path. If the daemon ever exposes an explicit capability signal in the Ping response (e.g. a Builder-Version-style header advertising native gRPC, which an intermediary that doesn't relay HTTP/2 would strip or rewrite), it should replace both the version gate and the probe.

- How to verify it

go test ./cli/grpc/... covers the three paths (h2c over the client dialer, TLS+ALPN, legacy hijack fallback) against a real gRPC server.

- Human readable description for the release notes

Created with: Claude Code

ndeloof and others added 2 commits August 12, 2026 16:17
Allow packages outside cli/context/docker to build a TLS client
configuration for a docker endpoint, such as the new cli/grpc package
which needs it to establish ALPN-negotiated HTTP/2 connections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Alongside its HTTP API, the daemon serves gRPC services on the same
endpoint (e.g. BuildKit's control API, and services published by daemon
extensions in the future). Since API v1.53 gRPC is served natively over
HTTP/2: h2c on non-TLS connections, ALPN-negotiated HTTP/2 on TLS
connections. Older daemons only expose gRPC through the deprecated
"POST /grpc" HTTP/1.1 upgrade endpoint.

Connect() hides those transport details behind the current docker
context: it reuses the API client's dialer (unix/npipe sockets, plain
tcp, ssh:// and other connection helpers) for h2c, performs its own
ALPN-negotiated TLS handshake for tcp:// endpoints with TLS material,
and falls back to the legacy upgrade endpoint when the daemon is older
than API v1.53.

This gives CLI plugins such as buildx and compose a single, shared way
to reach the daemon's gRPC services; buildx currently hand-rolls the
legacy hijack path.

google.golang.org/grpc moves from indirect to direct dependency; the
grpc/health packages are vendored for the tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.68852% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cli/grpc/grpc.go 81.35% 8 Missing and 3 partials ⚠️
cli/context/docker/load.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Fix revive's import-shadowing on the DialHijack parameter name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ndeloof added a commit to docker/compose that referenced this pull request Aug 12, 2026
Replace docker/cli with the cli-grpc branch (ndeloof/cli@ed1a1a1c0a81)
providing cli/grpc.Connect, the single way for CLI plugins to establish
a gRPC connection to the daemon's API endpoint whatever the transport
of the current context.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ndeloof added a commit to docker/compose that referenced this pull request Aug 12, 2026
Demo counterpart of the in-tree compose extension built on top of the
moby extensions framework (moby/moby#53021): at the start of up, push
the resolved Compose project (YAML) to the com.docker.compose.stack.v0
gRPC service the extended daemon exposes on its API endpoint.

The connection goes through cli/grpc.Connect (docker/cli#7183), which
hides the transport of the current context (unix/npipe socket, tcp
with or without TLS, connection helpers) and the native-h2c vs legacy
upgrade negotiation. Best effort: Unimplemented (no extension) and
transport errors (no h2c) are debug-logged and up proceeds unchanged.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
An intermediary between the client and the daemon may not relay HTTP/2
even though the daemon's API version advertises native gRPC support —
Docker Desktop's API proxy does not, at the time of writing. Probe
native connections with a health-check RPC (any RPC-level outcome,
including Unimplemented, proves the transport) and fall back to the
legacy upgrade endpoint on transport-level failure.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ndeloof added a commit to docker/compose that referenced this pull request Aug 12, 2026
Replace docker/cli with the cli-grpc branch (ndeloof/cli@b90d88ea3ad1)
providing cli/grpc.Connect, the single way for CLI plugins to establish
a gRPC connection to the daemon's API endpoint whatever the transport
of the current context, falling back to the legacy upgrade endpoint
when an intermediary does not relay HTTP/2.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ndeloof added a commit to docker/compose that referenced this pull request Aug 12, 2026
Demo counterpart of the in-tree compose extension built on top of the
moby extensions framework (moby/moby#53021): at the start of up, push
the resolved Compose project (YAML) to the com.docker.compose.stack.v0
gRPC service the extended daemon exposes on its API endpoint.

The connection goes through cli/grpc.Connect (docker/cli#7183), which
hides the transport of the current context (unix/npipe socket, tcp
with or without TLS, connection helpers) and the native-h2c vs legacy
upgrade negotiation. Best effort: Unimplemented (no extension) and
transport errors are debug-logged and up proceeds unchanged.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
@ndeloof

ndeloof commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Pushed b90d88e: native connections are now probed with a health-check RPC and fall back to the legacy upgrade endpoint on transport-level failure. Rationale: an intermediary may not relay HTTP/2 even when the daemon's API version advertises native support — Docker Desktop's API proxy does exactly that (engine reports API 1.55, h2c dial fails with "server preface … looked like an HTTP/1.1 header", while the legacy upgrade reaches the engine's gRPC server fine, so it is the proxy, not the engine). Covered by TestDialNativeFallsBackToLegacy, and validated end to end from docker/compose#14040: against Docker Desktop the publish now degrades to a clean Unimplemented instead of a transport error, and the native path is unchanged against an h2c-capable daemon.

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.

2 participants