Conversation
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
Lower-confidence findings (not posted inline)
- [medium→low] pkg/compose/plugins.go:395 —
get-imagerequest handled for any plugin command without command-context guard (confidence: weak 52/100)
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
docker-agent
left a comment
There was a problem hiding this comment.
This review covers only the commits pushed since d7fc6a1 (the previous completed review).
Assessment: 🟢 APPROVE
The incremental diff (92 lines across 3 files) makes two targeted fixes:
-
plugins.go —
handlePluginMessagegains acommandparameter soget-imagerequests are rejected with a protocol error when sent outside thepullcommand, preventing image-export stalls inup/down/stop. Thecommandstring is passed by value and is thread-safe. -
pull.go —
ensureProviderImagesnow runs unconditionally before theerr != nilcheck (fixing the previously reported finding): a regular-service pull failure no longer silently skips provider image distribution. When both fail,errors.Join(err, providerErr)surfaces both. UnderIgnoreFailures=true,providerErris suppressed symmetrically with regular service pull errors — consistent with the semantics documented in the prior review thread resolution. -
provider_images_test.go — New unit test
TestExecutePlugin_GetImageRefusedOutsidePullvalidates the protocol restriction. Theassert.ErrorContainssubstring ("get-image is only supported during the pull command") correctly matches the actual error message from plugins.go ("invalid message from plugin: get-image is only supported during the pull command").
No new bugs found in the + lines.
7b812e0 to
96d14a1
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
96d14a1 to
5a7bc6f
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
Provider image-phase integration is well-structured overall and the chunked-streaming protocol is sound. Two correctness issues were found in the new code.
5a7bc6f to
a4da6e1
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
…ution Providers opt into image distribution by declaring a pull block in their metadata — the presence of the command block is the declaration of support, generalizing the stop precedent. During its pull command a provider can request the service image from the local daemon with a get-image message (optionally platform-narrowed); compose answers on stdin with one image-stream JSON line then the tar as HTTP/1.1 chunked data (RFC 9112): length-prefixed blocks need no in-band delimiter in binary data, the zero-length chunk marks a COMPLETE transfer, and any stock chunked reader consumes it. There is deliberately no trailer nor final CRLF after the zero chunk — a stock reader stops there without consuming further bytes, so the next stdin answer starts clean for a provider requesting several images. An export failure is announced in-band through the error field so the provider is never left waiting; a failure after the announce closes the answer channel so the truncation is observable as EOF mid-chunk instead of a stream nobody will finish. The stream is exclusive on stdin for its whole duration — the framing invariant behind the lock. get-image is scoped to the pull command: emitted during up, down or stop it is a protocol error rather than served, so an image export can never stall another lifecycle command. Message dispatch moves out of executePlugin into handlePluginMessage, which also keeps the function under the complexity threshold. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ensureImagesExists and docker compose pull now give provider-backed services their turn: compose invokes the provider's pull command with the image identity and the state of the local daemon cache (--image/--digest/--created), plus two verdicts compose alone can compute so providers never re-implement that arbitration: - --source: the authority for this invocation — local (the daemon's image is the desired state: build-only service, pull_policy build, or an image the current run just built) or registry (resolve the reference upstream — including the CI workflow where build is only the recipe used to publish the image consumers pull); - --policy: missing on the up path (a usable version suffices) or always on compose pull (ensure freshness of the authority). digest/created describe the cache, they are not instructions: the provider persists them as the bookkeeping keys of what it ingested — digest as identity test, created as the ordering fallback for backends that cannot preserve digests. Providers without the metadata block are skipped: behavior unchanged. Providers are independent, so their pulls run without fail-fast cancellation — the first failure no longer kills its siblings mid-run and mangles their reports into context-canceled noise; every provider runs to completion and every failure is reported, prefixed by service. On the pull path the provider phase runs unconditionally — skipping it because an unrelated service failed to pull would silently leave provider runtimes stale — and its error is a per-service pull error, following their exact regime: reported with them, suppressed by IgnoreFailures. A fatal errgroup error is reported joined with the accumulated per-service context instead of dropping it. The provider runs under the root context: the errgroup context is canceled once Wait returns and would kill the provider process on the spot. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
startService errored with 'no container to start' whenever the project-wide container listing came back empty, before the per-service filtering that lets provider services pass through. A project whose services are all provider-backed legitimately reaches the start phase with zero containers. The empty-list error is now softened for provider services only — a relay container deployed for published endpoints still goes through the regular start path. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
a4da6e1 to
f482d73
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
extension.md gains the Image distribution section: invocation contract (--image/--digest/--created/--source/--policy), the get-image request and its chunked image-stream answer (zero-chunk terminated, stdin exclusivity and drain-before-next-answer contract), the two-branch decision guidance (digest as identity, created as ordering fallback, reproducible builds caveat), and the support-by-presence metadata convention is now stated as the general rule. The example provider implements pull end to end — request, stock chunked reader, hash recorded at PROVIDER_PULL_MARKER — and the e2e scenario locks the whole path: up builds the service image and streams it to the provider under the local verdict, compose pull re-invokes it under the freshness contract. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
f482d73 to
cc4504f
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The provider image distribution implementation is solid. The chunked-transfer protocol, mutex discipline, and error propagation are all well-handled. The two drafter hypotheses about a trailing CRLF mismatch between ChunkedWriter.Close() and the documented protocol were investigated and found to be false positives: Go's httputil.ChunkedWriter.Close() writes only "0\r\n" (not "0\r\n\r\n") — confirmed from the Go 1.26.3 source — and the code comment and docs/extension.md are correct. ChunkedReader returns EOF when it reads the zero-length chunk without consuming any additional bytes, so the wire format is internally consistent and the test assertion ("no byte may follow the terminating zero chunk") is valid.
Lower-confidence findings (not posted inline)
-
[low]
pkg/compose/pull.go:~86–93 —ensureProviderImagesunreachable whenpullServiceImages/pullHookImagesreturns a fatal scheduling error (unverified low severity)
The comment at line 102 states providers run "even when a regular pull failed", which is accurate for per-image pull failures accumulated inp.pullErrors. However, ifpullServiceImagesorpullHookImagesreturns a non-nil scheduling error (e.g. ashouldPullImageparse failure on a badpull_policyvalue), the early-exit at line 93 skipsensureProviderImagesentirely. In practice this edge case only triggers on configuration errors that would abort the command anyway, so real-world impact is minimal. -
[low]
pkg/compose/provider_images.go:97–107 —providerImageSourcealways returns"local"for build-only services duringcompose pull --pull=always(unverified low severity)
For a service withbuild:declared and noimage:name,providerImageSourcealways returnsproviderImageSourceLocalregardless of whether the image was actually rebuilt in this run. Oncompose pull,builtisnil, sojustBuiltis alwaysfalse— and sinceservice.Image == "", line 101 returns"local". This is arguably correct design (build-only services have no registry authority), but a provider receivingsource=local, policy=alwaysmay re-ingest the same unchanged local image on everycompose pull, giving false freshness signals. A brief doc comment clarifying this intent would help provider authors.
What this PR does, in one sentence
Provider-backed services take part in the image phase: compose hands the provider the identity of the service image and — on request — streams it the image bytes, so a
build:orimage:declared on a provider service actually reaches the provider's runtime.Context
A provider service can declare
build:today: compose builds the image… into the local daemon, where the provider never sees it. Providers whose runtime lives elsewhere (a sandbox VM with its own daemon, a cloud backend) have no way to receive a locally-built image, and no say in the image phase at all —docker compose pullskips them entirely. The only workarounds are out-of-band: push to a registry the provider can reach, or manualdocker save | loadplumbing.What the PR brings
Providers opt in by declaring a
pullblock in theirmetadataoutput — the presence of the command block is the declaration of support, the same conventionstopalready uses (now documented as the general rule). For opted-in providers, compose invokes<provider> compose pullduring the image phase ofup(after any build) and ondocker compose pull, passing:--image) and the state of the local daemon cache (--digest/--created, when present) — facts to persist as bookkeeping keys, not instructions: digest as identity test,createdas the ordering fallback for backends that cannot preserve digests (a rebuilt local image must be detectable as newer even by a backend that transforms what it ingests);--source(local: the daemon's image is the desired state — build-only service,pull_policy: build, or just built by this run; registry: resolve upstream, which covers the build-as-CI-recipe workflow where consumers never build) and--policy(missing onup: a usable version suffices; always onpull: ensure freshness).When the provider needs the local bytes, it sends a
get-imagerequest and compose answers on its stdin with one JSON line then the image tar as an HTTP/1.1 chunked body (RFC 9112): length-prefixed blocks need no in-band delimiter in binary data, the zero-length chunk marks a complete transfer (a truncated stream must be discarded), and any language's stock chunked reader consumes it. Export failures are announced in-band so the provider is never left waiting.Guardrails: providers without the metadata block see zero behavior change; the wire format is locked by unit tests (byte-identical round-trip on delimiter-hostile payloads, error announce, end-to-end against a fake provider process); an e2e scenario drives the full path through the example provider (up builds and streams under the local verdict,
compose pullre-invokes under the freshness contract). The PR also fixes a pre-existing gap the scenario exposed: a project made only of provider services failed the start phase with "no container to start" — softened for provider services only, the endpoint-relay still going through the regular start path.Why this is the right next brick
This is deliberately the provider-initiated design: the image phase is where compose already makes images available to where services run, and the digest/created negotiation gives idempotence for free (no re-streaming on every
up). A side-channel transport for very large images and the pull-fails→build fallback chain can layer on later without breaking the announced-answer protocol (encodingfield).🤖 Generated with Claude Code