Skip to content

feat(provider): get-service-config, addhost, and endpoint conventions - #14175

Draft
ndeloof wants to merge 1 commit into
docker:mainfrom
ndeloof:provider-control-channel
Draft

feat(provider): get-service-config, addhost, and endpoint conventions#14175
ndeloof wants to merge 1 commit into
docker:mainfrom
ndeloof:provider-control-channel

Conversation

@ndeloof

@ndeloof ndeloof commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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 an extra_hosts entry into every dependent service — typically the provider's own service name aliased to host-gateway, so consumers keep using the service name while the resource actually lives on the host.
  • Links-style endpoint variables (docs): the recommended setenv convention PORT_<container-port>_<proto> (+_ADDR/_PORT/_PROTO, and a primary PORT), 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 sbx provider (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

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 marshalled types.ServiceConfig ever 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.

Comment thread docs/examples/provider.go Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[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

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.94737% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/compose/plugins.go 78.37% 4 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@ndeloof
ndeloof force-pushed the provider-control-channel branch from cc1654d to a6f92eb Compare September 3, 2026 17:02
@ndeloof ndeloof changed the title feat(provider): stdio control channel serving the resolved service config feat(provider): get-service-config message serving the resolved service config Sep 3, 2026
@ndeloof
ndeloof force-pushed the provider-control-channel branch from a6f92eb to 279831f Compare September 4, 2026 09:37
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>
@ndeloof
ndeloof force-pushed the provider-control-channel branch from 279831f to 3b98f63 Compare September 4, 2026 10:06
@ndeloof ndeloof changed the title feat(provider): get-service-config message serving the resolved service config feat(provider): get-service-config, addhost, and endpoint conventions Sep 4, 2026
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