feat(provider): publish-endpoint deploys a network relay for provider services - #14193
feat(provider): publish-endpoint deploys a network relay for provider services#14193ndeloof wants to merge 1 commit into
Conversation
ee1f177 to
243b41f
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
This PR introduces a well-structured relay mechanism for provider services — the core logic in relay.go, plugins.go, and relay/main.go is sound. One low-severity issue was found in the example/demo code only.
c0c356c to
fee8789
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
fea2480 to
893cb95
Compare
|
/review |
|
👋 Heads up: The |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
Two findings in the new relay code introduced by this PR.
893cb95 to
51b5795
Compare
|
/review |
|
👋 Heads up: The |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
Two medium-severity correctness/reliability issues and two low-severity issues were found in the new relay code.
Findings summary:
relay/main.go:wg.Wait()does not drain activeforward()connections on shutdown (medium, 🟢 strong 100/100)pkg/compose/relay.go: paused relay with matching identity is force-removed instead of unpaused (medium, 🟢 strong 97/100)pkg/compose/relay.go: missing existence check beforeproject.Networks[key].Name(low, 🟡 moderate 57/100)docs/examples/provider.go:bufio.ReadStringhas no timeout when reading serve-demo address (low, 🟢 strong 100/100)
51b5795 to
9a1e73c
Compare
|
/review |
|
👋 Heads up: The |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🔴 CRITICAL
1 critical finding (high-severity CONFIRMED bug) and 1 minor finding (low-severity CONFIRMED).
Summary: The PR introduces a solid relay mechanism for provider services. The main correctness issue is an orphan container left behind when network attachment partially fails; the Dockerfile has a dead ARG that should be wired up.
9a1e73c to
4bbd5eb
Compare
|
/review |
|
👋 Heads up: The |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
Two medium-confidence findings in the new relay code introduced by this PR.
4bbd5eb to
f7054f2
Compare
|
/review |
|
👋 Heads up: The |
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
One medium-severity finding: the relay guard introduced by this PR is incomplete in pkg/compose/cp.go — the default branch (common cp usage without --index) skips the checkRelayTarget check that was correctly added to the options.Index > 0 branch.
f7054f2 to
83afdc3
Compare
|
/review |
|
👋 Heads up: The |
docker-agent
left a comment
There was a problem hiding this comment.
🟢 No issues found — LGTM! View logs.
83afdc3 to
f0431d0
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
One confirmed issue found in the new relay deployment code.
| } | ||
| } | ||
| if command == "up" && len(variables.endpoints) > 0 { | ||
| if err := s.ensureServiceRelay(ctx, project, service, variables.endpoints); err != nil { |
There was a problem hiding this comment.
[medium] ensureServiceRelay called while holding global mutex — serializes concurrent provider deployments
runPlugin acquires mux (line 96) to protect the shared project.Services env-var writes, then—under the same defer mux.Unlock()—calls ensureServiceRelay (line 114) which performs multiple Docker API operations:
findRelayContainer→ContainerList(network I/O)ContainerCreate(network I/O)pullRelayImage→ImagePull+ stream drain (potentially minutes on first pull)ContainerStart(network I/O)waitRelayRemoved→ 250 ms polling loop for up to 30 seconds
runPlugin is dispatched concurrently for all provider services by the plan executor's errgroup (see executor.go line 107). With the mutex held across these slow Docker API calls, every concurrent provider is forced to wait for the slowest one—including its image pull. Before this PR, the mutex only guarded a tight in-memory loop.
Fix: release mux before calling ensureServiceRelay. The relay deployment reads and writes only Docker API state; it does not need the mutex that protects project.Services.
| if err := s.ensureServiceRelay(ctx, project, service, variables.endpoints); err != nil { | |
| if command == "up" && len(variables.endpoints) > 0 { | |
| mux.Unlock() | |
| err := s.ensureServiceRelay(ctx, project, service, variables.endpoints) | |
| mux.Lock() | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| return nil |
Note: the
defer mux.Unlock()must be replaced with an explicitUnlockaround the env-var loop if taking this approach—or restructure soensureServiceRelayis called aftermuxis released naturally.
| Confidence | Score |
|---|---|
| 🟢 strong | 97/100 |
… services
A provider's resource lives outside the compose network: consumers could
only reach it through injected variables carrying a host-published
address — nothing like the compose-native experience of addressing a
service by name at its well-known port.
A provider may now publish where each endpoint of its resource actually
listens:
{"type": "publish-endpoint", "message": "80=localhost:49152"}
The endpoint is announced as seen from the provider's host: the relay —
the component that knows it runs inside a container — rewrites loopback
or unspecified upstream hosts to host.docker.internal (resolved through
its injected host-gateway extra_host); routable addresses pass through.
When at least one endpoint is published, compose deploys a relay
container in place of the service: a minimal TCP forwarder (new relay/
directory, published as docker/compose-relay, overridable with
COMPOSE_RELAY_IMAGE for internal registries) joining the networks of the
services that depend on the provider service, aliased with the service
name. Consumers then use http://<service>:<port> as if the service were
a regular container.
The relay is a first-class project container — canonical name, standard
compose labels including config-hash (label-driven commands run without
the compose file keep seeing the service: ps, logs, stop, down) — plus
the com.docker.compose.relay label declaring its role:
- the reconciler already leaves provider services' containers alone, and
the relay's identity hash (image + routes) makes up idempotent: kept
when routes are unchanged, recreated otherwise;
- process-level commands (exec, cp) refuse a relay — there is no service
process in it to act on;
- the up monitor excludes relays from the containers whose termination
ends an attached up: they are long-lived infrastructure and would
otherwise keep 'up' waiting forever.
The example provider demonstrates the flow behind PROVIDER_DEMO_ENDPOINT
(a detached helper serving a fixed HTTP response), backed by an e2e
scenario asserting the compose-native address works and exec is refused.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
f0431d0 to
af2aaf6
Compare
A provider's resource lives outside the compose network: consumers could only reach it through injected variables carrying a host-published address. This adds the transparent path:
publish-endpointprovider message:{"type": "publish-endpoint", "message": "80=host.docker.internal:49152"}— container port consumers know on the left, real location on the right.docker/compose-relay(newrelay/directory: static Go TCP forwarder,FROM scratch, bake targetrelay-image,COMPOSE_RELAY_IMAGEoverride for internal registries) in place of the service — canonical<project>-<service>-1name, service alias on the networks of the depending services. Consumers usehttp://<service>:<port>, no injected variables involved.ps,logs,stop,down) keep seeing the service — pluscom.docker.compose.relay(value: identity hash of image+routes) declaring its role:upidempotent: relay kept when routes unchanged, recreated otherwise (reconciler already leaves provider services' containers alone);exec/cprefuse a relay (no service process to act on);upmonitor excludes relays from the containers whose termination ends the command — they are long-lived infrastructure.This benefits every provider: the example provider demonstrates the flow (detached HTTP helper + publish-endpoint), covered by an e2e scenario asserting
http://dbworks from a consumer andexec dbis refused, plus unit tests (message parsing, route identity, network selection, relay guard, image override).Standalone — no dependency on #14175 (the two protocol additions are orthogonal).
Publishing note for maintainers:
docker/compose-relayis a new image to publish on Docker Hub under the docker/ organization (bake target included; release wiring to be added to CI).🤖 Generated with Claude Code