feat(provider): get-service-config, addhost, and endpoint conventions - #14175
feat(provider): get-service-config, addhost, and endpoint conventions#14175ndeloof wants to merge 1 commit into
Conversation
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The control-channel implementation is well-structured. The synchronous request/response loop is simple and correct for the typical case. One finding in the example code, and one lower-confidence note below.
Lower-confidence findings (not posted inline)
- [low] pkg/compose/plugins.go:208 — Potential deadlock when large service config response exceeds OS pipe buffer (confidence: weak 52/100). The synchronous
responses.Encode()write to the provider's stdin happens in the same goroutine that reads from stdout. If the marshalledtypes.ServiceConfigever exceeds the OS pipe buffer (~64 KB on Linux), and the provider is blocked waiting for this response before writing more output, neither end makes progress. Typical configs are well under 1 KB so this is unlikely in practice, but a goroutine for the write would eliminate the risk entirely.
| fmt.Printf(`{ "type": "error", "message": "invalid service config: %v" }%s`, err, lineSeparator) | ||
| return | ||
| } | ||
| fmt.Printf(`{ "type": "setenv", "message": "CONFIG_TYPE=%s" }%s`, config.Provider.Type, lineSeparator) |
There was a problem hiding this comment.
[low] JSON injection: unescaped config.Provider.Type in hand-crafted JSON string
The example emits a setenv message by interpolating config.Provider.Type directly into a raw string literal:
fmt.Printf(`{ "type": "setenv", "message": "CONFIG_TYPE=%s" }%s`, config.Provider.Type, lineSeparator)If config.Provider.Type contains a " (double-quote), \ (backslash), or a newline, the emitted line is not valid JSON. Compose reads it with json.NewDecoder(stdout).Decode(&msg) — a decode error causes executePlugin to return with an error, aborting the provider interaction entirely.
While provider type values are typically simple identifiers like "sbx", the example code is what users copy when writing their own providers. Following this pattern with user-controlled or URL-like values propagates the bug into real implementations.
Use a struct and json.Marshal (or json.NewEncoder) to produce the message safely:
type msg struct {
Type string `json:"type"`
Message string `json:"message"`
}
b, _ := json.Marshal(msg{Type: "setenv", Message: "CONFIG_TYPE=" + config.Provider.Type})
fmt.Printf("%s%s", b, lineSeparator)| Confidence | Score |
|---|---|
| 🟡 moderate | 75/100 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
cc1654d to
a6f92eb
Compare
a6f92eb to
279831f
Compare
Providers could not see the definition of the service they manage, nor
make their resource addressable from consuming services.
- A provider may emit {"type": "get-service-config"} on stdout; compose
answers on the provider's stdin with one JSON line holding the
resolved canonical configuration of the provider's own service,
straight from the in-memory model. Detection is by construction: a
compose that predates the message aborts on it and never writes to
stdin, so the provider treats EOF as 'unsupported, upgrade compose'.
- A provider may emit {"type": "addhost", "message": "name=value"} to
inject an extra_hosts entry into every dependent service — typically
its own service name aliased to host-gateway, so consumers keep
addressing it by the name they already use while the resource
actually lives on the host. Injection relies on plan-node copies
sharing the underlying maps, so provider-dependent services get
their ExtraHosts materialized before the plan is built.
- docs/extension.md documents both, plus the recommended links-style
endpoint variables convention (PORT_<port>_<proto>[_ADDR|_PORT|_PROTO]
over setenv) so consumers look endpoints up by the container port
they know while providers assign actual host ports freely.
The example provider demonstrates the round trip, backed by an e2e
scenario; unit tests drive executePlugin against a helper-process
provider and cover the injection.
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
279831f to
3b98f63
Compare
Providers had no way to see the definition of the service they manage, nor to make their resource addressable from consuming services. Two protocol additions and one documented convention:
get-service-config: the provider emits{"type": "get-service-config"}on stdout; compose answers on the provider's stdin with one JSON line — the resolved canonical configuration of the provider's own service, straight from the in-memory model. Detection is by construction (no capability env var): an older compose aborts on the unknown message and never writes to stdin, so the provider treats EOF as "unsupported, upgrade Docker Compose".addhost:{"type": "addhost", "message": "name=value"}injects anextra_hostsentry into every dependent service — typically the provider's own service name aliased tohost-gateway, so consumers keep using the service name while the resource actually lives on the host.setenvconventionPORT_<container-port>_<proto>(+_ADDR/_PORT/_PROTO, and a primaryPORT), so consumers look endpoints up by the port they know while providers assign actual host ports freely — no host-port collisions between projects/providers.First consumer: the
sbxprovider (docker/sandboxes#5650), which converts the resolved service definition into a sandbox and exposes its published ports through the alias + links variables.Docs updated (
docs/extension.md), example provider demonstrates the round trip, covered by unit tests (helper-process provider, injection) and an e2e scenario.🤖 Generated with Claude Code