get-relay-info tells providers where to bind for the relay - #14258
Conversation
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🔴 CRITICAL
One high-severity data race confirmed in the new GetRelayInfoType handler.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
A provider running its service LOCALLY has to pick the address its published endpoint binds, and on a standalone Linux engine no address is both relay-reachable and off the LAN (docker#14257): host.docker.internal resolves there to the bridge gateway, which a loopback-only listener cannot accept, while the wildcard exposes the port on every host interface. The bundled example provider worked around it by binding 0.0.0.0. The new get-relay-info message closes that gap, with compose owning the platform knowledge so the provider just binds what is announced. Compose answers with one JSON line listing the networks the relay would join — the dependents' networks, exactly as selected for the relay deployment — each with the address a locally-run endpoint should bind to be reachable from the relay: the network's engine-assigned gateway on a standalone engine (an address the host owns on that bridge, reachable from the relay but not from the LAN), and 127.0.0.1 under Docker Desktop — the networks live inside the VM there, and the host's own loopback is, factually, where a host process is reached through the Desktop proxy. Resolution is lazy (Desktop detection and network inspects run only when a provider asks — a remote-resource provider never does) and best-effort: a missing gateway means "bind elsewhere". Compose also announces the message types it accepts in the COMPOSE_PROVIDER_MESSAGES environment variable of the provider process: an unknown message stays a fatal protocol error by design (a provider REQUIRING an unsupported message must fail loudly), and the announcement is how a provider adapts instead of failing. The example provider binds the first announced gateway, falling back to the wildcard only when none is announced; through it, TestProviderPublishEndpoint exercises the gateway path on Linux CI and the loopback path on Docker Desktop. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
eebe9ad to
9badbe3
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
glours
left a comment
There was a problem hiding this comment.
TestExecutePlugin_GetRelayInfo and TestExecutePlugin_GetRelayInfoDesktop cover the happy path and the Desktop path, but nothing exercises relayInfo's "gateway unresolved" branch (NetworkInspect error, or an IPAM config with no valid IPv4 gateway) — the exact case the omitempty/best-effort contract in relay.go is built around. Can you add a case mocking NetworkInspect to fail (see notFoundError in executor_test.go, already used this way in observed_state_test.go:387) and asserting the network is still listed with Gateway empty? Without it, a later change to the err == nil/cfg.Gateway.IsValid() guards could start sending a wrong non-empty gateway with nothing catching it.
Address glours review on docker#14258: TestExecutePlugin_GetRelayInfo and TestExecutePlugin_GetRelayInfoDesktop only exercised the happy path. Add TestExecutePlugin_GetRelayInfoUnresolvedGateway to cover the best-effort contract relay.go omitempty is built around: a NetworkInspect failure, an IPv6-only gateway, and no gateway at all must each still list the network with Gateway left empty. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
docker-agent
left a comment
There was a problem hiding this comment.
This review covers only the commits pushed since 9badbe3.
Assessment: 🟢 APPROVE
The single change in this incremental diff — the new TestExecutePlugin_GetRelayInfoUnresolvedGateway table-driven test — is well-constructed. It correctly covers all three best-effort contract branches for unresolved gateways: a failing NetworkInspect, an IPv6-only IPAM gateway, and an absent IPAM gateway. Mock expectations, variable-name assertions (RELAY_NETWORK / RELAY_GATEWAY), the len(variables.prefixed) == 2 invariant, and the subprocess helper pattern all match the production code. No bugs found in the diff.
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
Addresses docker-agent review on docker#14258 (discussion_r4092089046): when ReadString fails after Start() succeeds, the pipe read-end was leaked -- nothing else calls Wait() on this deliberately long-lived subprocess. Mirrors the existing close on the Start() failure path. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
docker-agent
left a comment
There was a problem hiding this comment.
This review covers only the commits pushed since 162ebb8.
Assessment: 🟢 APPROVE
The single change in this incremental diff — closing the stdout pipe's read-end when ReadString fails in serveDemo — is correct and introduces no new issues.
stdout is an io.ReadCloser returned by server.StdoutPipe(). Because Wait() is intentionally never called (the subprocess outlives the function), nothing else would release the pipe's read-end file descriptor on the error path. The explicit _ = stdout.Close() cleanly plugs that leak, mirroring the identical pattern already present on the server.Start() failure path. Discarding the Close() error is appropriate since the function is already returning a more meaningful error from ReadString, and there is no double-close risk.
Address glours review on #14258: TestExecutePlugin_GetRelayInfo and TestExecutePlugin_GetRelayInfoDesktop only exercised the happy path. Add TestExecutePlugin_GetRelayInfoUnresolvedGateway to cover the best-effort contract relay.go omitempty is built around: a NetworkInspect failure, an IPv6-only gateway, and no gateway at all must each still list the network with Gateway left empty. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
What this PR does, in one sentence
A provider running its service locally can now ask Compose where to bind its published endpoint (
get-relay-info), so the relay reaches it on every platform without exposing the port on the LAN.Closes #14257
Context
When a provider publishes an endpoint, Compose deploys a relay so dependents reach the service at its compose-native address. But the provider has to pick the address its endpoint binds, and on a standalone Linux engine no address is both relay-reachable and off the LAN:
host.docker.internalresolves there to the bridge gateway, which a loopback-only listener cannot accept, while the wildcard exposes the port on every host interface. The bundled example provider works around it by binding0.0.0.0— trading LAN exposure for reachability — and a provider choosing loopback instead is silently limited to Docker Desktop.What the PR brings
The approach retained: Compose owns the platform knowledge, the provider just binds what is announced. A new
get-relay-infomessage answers with the networks the relay would join — the dependents' networks, exactly as selected for the relay deployment — each carrying the address a locally-run endpoint should bind to be reachable from the relay:127.0.0.1— the networks live inside the VM there, and the host's own loopback is, factually, where a host process is reached through the Desktop proxy.Guardrails that bound the change:
gatewayfield omitted — the provider falls back to a bind of its choice;COMPOSE_PROVIDER_MESSAGESenvironment variable of the provider process, letting providers adapt instead;The example provider demonstrates the pattern — bind the first announced gateway, wildcard only when none is announced — which also closes the coverage gap noted in #14257:
TestProviderPublishEndpointnow exercises the gateway path on Linux CI and the loopback path on Docker Desktop, instead of only ever testing a wildcard bind.