`bridge convert -o <dir>` unconditionally ran `os.RemoveAll` on the
output directory and discarded any error, so a typo (`-o .`, `-o
$PWD`, `-o ~`) silently destroyed unrelated user data.
An empty or missing output directory is still created silently, but a
non-empty one now requires explicit confirmation (prompted
interactively, or via the new `--yes` flag for scripts/CI) before
being wiped.
Signed-off-by: Guillaume Lours <[email protected]>
full diff: https://github.com/golang/crypto/compare/v0.55.0...v0.56.0
We have tagged version v0.56.0 of golang.org/x/crypto in
order to address the following security issues:
- ssh: prevent DoS on deadlocked established channel
Previously, after a channel has been established, a
malicious peer could send crafted messages that would
deadlock the entire connection.
Now, we handle all RFC 4254 channel messages; global
requests are handled explicitly. Then, treat all other
messages as a protocol error and tear the connection
down instead of buffering and blocking.
Thanks to Will Mortensen for reporting this issue.
This is CVE-2026-56855 and Go issue https://go.dev/issue/81317.
- ssh: prevent DoS on deadlocked undecided channel
Previously, a channel registered in the mux's chanList is
not usable until it is established. A malicious peer was
able flood the channel's incomingRequests, deadlocking the
entire connection.
Now, we add an atomic established state, set when a channel
becomes usable. Until such a time, handlePacket drops every
packet other than the open confirmation/failure, without
blocking and without tearing down the connection.
Thanks to Will Mortensen for reporting this issue.
This is CVE-2026-78662 and Go issue https://go.dev/issue/81316.
Signed-off-by: Sebastiaan van Stijn <[email protected]>
v0.0.6 dropped the Egress policyType/block from generated
NetworkPolicy manifests since it blocked CoreDNS traffic; update
the fixtures to match.
Signed-off-by: Ricardo Branco <[email protected]>
OTel SDK/exporter errors already go through docker/cli's own
otel.SetErrorHandler(debug.OTELErrorHandler), installed by the plugin
framework at startup. Our own handler, installed at package init time,
was always clobbered before any command ran (plugin.Run() sets its handler
after all inits execute), so it never had any effect - verified by tracing
the exact call order and reproducing the flag behavior against a built
binary.
Drop that dead handler along with the COMPOSE_OTEL_DEBUG env var it
introduced. The one genuinely-discarded error (the tracer shutdown/flush
error in wrapRunE) now goes through logrus like everything else in the
codebase: it's already gated by --debug/-D via logrus's own level, so no
explicit check is needed.
Signed-off-by: hiroto.toyoda <[email protected]>
Split SetRecursive into the existing per-GOOS watcher files instead of
a runtime check + nolint: the non-Windows stub never calls the
always-failing SetRecursive, and Windows failures now propagate as a
real error instead of silently downgrading to non-recursive.
Addresses review from thaJeztah on #14164.
Signed-off-by: Guillaume Lours <[email protected]>
- migrate gomodguard to gomodguard_v2 (gomodguard is deprecated since
v2.13 and warns on every run); same blocked modules, flat-list schema
- silence SA4023 in watcher_naive.go: tilt-dev/fsnotify SetRecursive
only succeeds on Windows, so on any single GOOS the newer staticcheck
proves the err == nil comparison constant — the cross-platform code
is correct, the finding is per-GOOS
Signed-off-by: Nicolas De Loof <[email protected]>
The PATH fallback of provider discovery appended a hardcoded ".exe" on
Windows, restricting providers to compiled binaries. exec.LookPath
already implements Windows executable resolution (PATHEXT), which finds
.exe but also .com/.bat/.cmd scripts — the suffix only narrowed the
lookup. Drop the executable() indirection and look up the bare name.
Note this only affects the PATH fallback: the docker CLI plugin channel
(docker-<name>) keeps requiring .exe on Windows, per docker/cli plugin
candidate rules.
Fixes#14157
Signed-off-by: Nicolas De Loof <[email protected]>
Building the dependency graph silently deleted unresolvable optional
depends_on references from the caller's project — and since DependsOn
is a map, even from projects sharing it. What later readers observed
depended on how many graphs had been built before them.
The pruning becomes an explicit step, applied once where the project
consumed by the engines is established — create and start — before the
graph, the dependency waits and the com.docker.compose.depends_on
label read the model, using Project.WithoutUnresolvedOptionalDependencies
introduced by compose-spec/compose-go#922 (go.mod bumped to the
post-merge commit). NewGraph now just skips the edge; required dangling
references still flow to its existing errors (naming the profiles that
would enable the dependency).
No behavior change; epic #14081, Lot 0 step 4 — precondition for a
canonical project object across plan phases.
Signed-off-by: Nicolas De Loof <[email protected]>
A container is orphaned when it is a one-off (compose run) that FINISHED
its task, or when it carries the project labels but its service is not
defined by the compose model — the typical leftover after the compose
file was edited. A still-RUNNING one-off is somebody's live session:
'up --remove-orphans' cleans up leftovers and must never kill it.
'down --remove-orphans' is the explicit stop-the-application action, so
it DOES take running one-offs down — through the per-service removal
loop, not the orphan branch.
That asymmetry was implemented but stated nowhere: isOrphaned's comment
only mentioned the model-absent half, down.go removed running one-offs
as an unexplained side effect of including one-offs in the listing, and
the observed-state collection silently dropped running one-offs with no
hint it was deliberate. No behavior change — the semantics are now
written at all three sites and pinned by tests: the predicate matrix,
the observed-state classification (a running one-off is neither a
service replica nor an orphan), and down's single stop+remove path for
a running one-off of a declared service.
Closes item C.4 of #14074.
Signed-off-by: Nicolas De Loof <[email protected]>
bridgeCommand and transformersCommand had no RunE, so an unknown
subcommand (e.g. "bridge zzz") fell through cobra's default
non-Runnable path: help printed, exit 0. Scripts couldn't distinguish
a typo from success. Mirrors the same fix already shipped on the
compose root command.
Signed-off-by: Guillaume Lours <[email protected]>
moby/buildkit#5558, which this test was disabled for, is fixed and
released well before the buildkit v0.26.3 currently vendored here.
Verified the re-enabled case passes (3 consecutive runs).
Signed-off-by: hiroto.toyoda <[email protected]>
Eleven comments in the reconciler and its neighbors justified decisions
by pointing at code that no longer exists ('matching the previous
ensureNetwork behavior', 'same way as convergence.go:138-160' — lines
that today hold unrelated code) instead of stating the behavior and its
reason. A reader following those pointers finds nothing, or worse,
something else.
Each is rewritten as a self-contained behavioral statement. Two carry
real weight:
- reconcileService's up-to-date branch now says explicitly that
starting containers is NOT the plan's job (a separate start phase in
start.go does it) — the previous wording invited 'fixing' the
StateCreated case;
- sortContainers now documents that its ordering IS the scale-down
policy (the tail it produces is what i >= expected trims), instead of
referencing stale line numbers.
Part of #14074 (A: the code misdescribes its own structure).
Signed-off-by: Nicolas De Loof <[email protected]>
The convergence type this file was named after was removed in fbea647b9,
but the file kept its name and its doc comment ('convergence manages
service's container lifecycle') ended up attached to the unrelated
getScale(). Anyone asked to 'change the convergence logic' lands here,
while the actual reconciliation lives in reconcile.go.
The file is renamed to service_containers.go with a header stating what
it really contains: the per-service container helpers shared by both
lifecycle engines (naming, service-reference resolution, dependency
waiting, container creation, startup). getScale gets a doc comment that
describes getScale.
Part of #14074 (A: the code misdescribes its own structure).
Signed-off-by: Nicolas De Loof <[email protected]>
The fenced Go blocks in docs/sdk.md become real code under
docs/examples/sdk, built by the ordinary module build: API drift now
breaks compilation. A unit test pins the markdown to the compiled
files byte-for-byte (first block = main.go from its package clause
down, second block = customService's body), so wording drift breaks
the test instead of silently rotting the docs.
Epic #14074, section G.
Signed-off-by: Nicolas De Loof <[email protected]>
The 'why, history and context' guidance was how the section-A findings of
epic #14074 were born: comments justifying code by referencing since-
deleted code. Rule 4 now asks for behavioral statements, and a Legibility
section spells out the map-must-stay-truthful rules for comments, file
names and error messages.
Signed-off-by: Nicolas De Loof <[email protected]>
The classic-builder rejections said 'set DOCKER_BUILDKIT=1 to use
BuildKit', a leftover from the internal BuildKit builder removed in
af579ebd4. Since then BuildKit builds go through the buildx plugin:
compose falls back to the classic builder when BuildKit is disabled
(DOCKER_BUILDKIT=0) OR when buildx is not installed — so the advertised
fix does nothing for users who are simply missing buildx.
The five messages now state the actual requirement (buildx installed,
DOCKER_BUILDKIT not forced to 0), and the silent-fallback warning when
buildx is missing now says what the fallback implies instead of a bare
'requires buildx plugin to be installed'.
Part of #14074 (A: the code misdescribes its own structure).
Signed-off-by: Nicolas De Loof <[email protected]>
AGENTS.md documents 'Test unit: go test ./pkg/...' and 'E2E tests:
go test -tags e2e ./pkg/e2e/'. Both statements were false: no file in
pkg/e2e carried an e2e build constraint, so the -tags flag was a no-op
and the 'unit' command actually launched the full e2e suite — requiring
a Docker daemon and the locally built binary, creating and destroying
containers, for up to 20 minutes. CI only avoided this by grepping e2e
out of the package list (Dockerfile).
Every *_test.go in pkg/e2e now carries //go:build e2e (merged with the
existing !windows constraints), and the Makefile e2e targets pass the
tag. The documented commands become true:
- go test ./pkg/... -> pkg/e2e reports [no test files]
- go test -tags e2e ./pkg/e2e -> runs the suite
AGENTS.md now also states the daemon/binary prerequisites explicitly.
Part of #14074 (A: the code misdescribes its own structure).
Signed-off-by: Nicolas De Loof <[email protected]>
sdk.md advertised a 'progress' package (progress.NewTTYWriter,
NewPlainWriter, NewJSONWriter, NewQuietWriter) that does not exist in
the repository, and typed WithEventProcessor against it. Anyone
following the doc writes code that cannot compile — or recreates the
phantom package.
The section now points at the real surface: api.EventProcessor, the
silent-by-default behavior when no processor is configured, and the
CLI's actual renderers in cmd/display (Full, Plain, JSON, Quiet) with
their real signatures.
Part of #14074 (A: the code misdescribes its own structure).
Signed-off-by: Nicolas De Loof <[email protected]>
When a service set `build.provenance: false` (or `sbom: false`), the
bake attest entry was silently omitted (the field is `omitempty`),
so BuildKit applied its defaults and attached a provenance attestation
anyway. The attestation changes the manifest-list digest on every build,
which caused containers to be needlessly recreated on `up --build`.
Emit `"type=<t>,disabled=true"` instead, which BuildKit's
`Attest.UnmarshalText` parses as `Disabled:true` and forwards to
BuildKit as an explicit disable signal.
Extract `appendAttest` to avoid duplicating the three-branch switch
for both provenance and sbom.
Fixes#14111
Signed-off-by: Guillaume Lours <[email protected]>
EncodedAuth now delegates the X-Registry-Auth wire encoding to
moby/api/pkg/authconfig (same module, already required) instead of
hand-rolling the JSON+base64url pair, and push.go reuses it instead of
its own copy. The CLI and moby AuthConfig structs are field-for-field
identical, so a direct type conversion bridges them.
The explicit GetAuthConfigKey normalization drops out of EncodedAuth:
docker/cli's configfile.GetAuthConfig normalizes the Docker Hub domain
itself. It remains for the OCI resolver, whose credential callback
receives the network host actually contacted (registry-1.docker.io),
which the configfile does not map.
Signed-off-by: Nicolas De Loof <[email protected]>
buildx was imported for two narrow purposes, both replaced by
docker/cli machinery compose already uses elsewhere:
- storeutil.GetProxyConfig -> configfile.ParseProxyConfig, the same
lookup docker build uses (also picks up ALL_PROXY and lowercase
variants)
- the dry-run image resolver (buildx builder + imagetools) -> a
DistributionInspect through the daemon with credentials from the CLI
config file, i.e. the same path a real pull/push would take; the
encoded-auth helper moves to internal/registry, shared with pull.go
bake is unaffected: it already execs the buildx CLI plugin. Dropping
the module also drops the transitive aws-sdk-go-v2 footprint
(-328 lines of go.sum).
Signed-off-by: Nicolas De Loof <[email protected]>
Fixes#14114: Done() blocked forever once the render goroutine had
already exited via ctx.Done() (e.g. after SIGTERM), since it sent on an
unbuffered channel with nobody left to receive.
Closing the channel instead of sending never blocks, but a bare close()
panics the moment ttyWriter runs a second Start/Done cycle on the same
shared bus - which already happens today (run's nested create/start,
publish's nested push, rm --stop's sequential stop then remove). Start
now hands out a fresh, once-only-closable signal each cycle instead of
a single shared channel, and run/publish stop opening a redundant
nested bracket around calls already inside one.
Signed-off-by: Guillaume Lours <[email protected]>
- compose_down.md: note that retained pre_start hook containers (those
kept after a hook failure for post-mortem) are automatically removed
by compose down, with the label filter to list them beforehand.
- compose_up.md: note the retention behavior on pre_start hook failure,
how to inspect the retained container, and that it is cleaned up on
the next compose up or compose down.
Signed-off-by: Guillaume Lours <[email protected]>
A pre_start hook container retained after failure (introduced in the
previous commit) is created without ConfigHashLabel so getContainers()
and every downstream compose operation (down, ps, stop, rm) is blind
to it. Without explicit cleanup it would survive compose down.
This commit adds removePreStartHookContainers(), called from down()
after regular container teardown and before network removal (so hook
containers are gone before their networks are removed). The cleanup is
project-wide on a full compose down; scoped to the requested services
when compose down <svc...> is used. Individual ContainerRemove errors
are logged at warn level and do not abort the teardown.
Changes:
- pkg/compose/down.go: removePreStartHookContainers helper + call in down()
- pkg/compose/filters.go: add //nolint:unparam to hookFilter (param is
architecturally correct; today only preStartHookType is in use)
- pkg/compose/down_test.go: update all existing down tests to expect the
new ContainerList call; add TestDownRemovesRetainedPreStartHookContainers
and TestDownHookContainerRemovalFailureIsNonFatal
Signed-off-by: Guillaume Lours <[email protected]>
A failing pre_start hook container is now retained (AutoRemove: false)
so operators can run 'docker logs <id>' and 'docker ps -a' to diagnose
the failure. On success the container is removed explicitly, mirroring
the old AutoRemove behaviour including anonymous volumes.
Before each pre_start run, stale hook containers from a previous failed
run are detected via project+service+HookLabel filters and force-removed
so they do not accumulate.
Changes:
- pkg/api/labels.go: add HookLabel (com.docker.compose.hook)
- pkg/compose/filters.go: add hookFilter helper
- pkg/compose/pre_start.go:
- AutoRemove: false in createPreStartContainer
- HookLabel added to container labels
- runPreStartHook: explicit ContainerRemove on success; retain on failure
- removeOrphanPreStartContainers: new helper called in runPreStart
- Tests: update all existing pre_start tests for the new flow; add
feature tests (success removes, failure retains, orphan cleanup) and
coverage-gap tests (lowestNumberedContainer, waitPreStart cancel,
preStartResultErr, streamPreStartLogs error paths, old-API network
paths, ExecCreate/Attach/Inspect errors, hookExitError branches)
Coverage after: hook.go 100%, pre_start.go most functions 100%
(was 89% and 64% respectively per Codecov delta).
Signed-off-by: Guillaume Lours <[email protected]>
On top of the output-tail infrastructure from the previous commit:
Stderr bias: split the combined output tail into separate stdout and stderr
buffers. stdcopy.StdCopy(wOut, wErr, reader) now routes each stream to its
own outputTail. The error message prefers stderr (where hooks write their
actual error) and falls back to stdout when stderr is empty:
db hook exited with status 1: Table 'service.sites' doesn't exist
A new hookExitError helper encapsulates this preference.
pre_start parity: streamPreStartLogs previously short-circuited and returned
immediately when listener was nil (detached mode / compose up -d), leaving
the tail buffers empty and failure errors without context. The log stream is
now always opened. The demultiplexed streams fill separate tailOut / tailErr
buffers. On non-zero exit the stderr-biased tail is appended to the error
(same behaviour as exec hooks).
Tests:
- TestRunHook_StderrBias: stdout noise stays out of the error when stderr
has content.
- TestPreStart_DetachedModeAttachesLogs: ContainerLogs is called even with
a nil listener.
- TestPreStart_FailureIncludesTail: a failing pre_start hook carries its
stderr output in the returned error.
Signed-off-by: Guillaume Lours <[email protected]>
startMx exists to serialize ContainerStart calls because the engine
assigns published ports from ranges non-atomically. It is held by the
plan executor's execStartContainer — a nearly dead code path (plans
only emit OpStartContainer for paused/dead containers) — and NOT by
startServiceContainer, the path every up/start actually runs. The
protection is thus inoperative today: concurrent services publishing
overlapping port ranges can race in the engine.
Take the mutex around the real ContainerStart call and document the
contract on the variable. Starts of different services were already
serialized per-service (replicas start sequentially); this serializes
the API call across services too, matching what the plan engine will
do once the start phase converges (#14081).
Part of #14074 (section C) / #14081 (lot 0).
Signed-off-by: Nicolas De Loof <[email protected]>
waitDependency returned nil whenever its context ended, so an expired
wait deadline was reported only when the 500ms poll ticker happened to
win the select race and the condition check then failed on the expired
context. Whether `up --wait --wait-timeout` failed or silently
succeeded after the timeout was a coin toss, and the "timeout waiting
for dependencies" mapping in waitDependencies was unreachable on the
direct path.
Return the context error when the deadline expired; a plain
cancellation (Ctrl-C) is still not reported as a dependency failure.
The unit test pins both sides deterministically (a deadline shorter
than the first tick used to be swallowed with certainty); the e2e
scenario documents the user-visible contract: `up --wait
--wait-timeout` on a never-healthy service fails with "application not
healthy after Xs" while the container keeps running.
Part of #14074 (section C) / #14081 (lot 0).
Signed-off-by: Nicolas De Loof <[email protected]>
`pkg/mocks` was only kept in sync by discipline: nothing failed when an
interface in `pkg/api` changed without regenerating. Add a
`mocks-validate` Dockerfile stage on the vendor-validate model, wire it
as a bake target, a `make validate-mocks` shortcut and a new entry in
the CI validate matrix.
Part of #14074 (section G).
Signed-off-by: Nicolas De Loof <[email protected]>
The `mocks` target still passed the pre-rename `Service` argument to
mockgen (the interface is `Compose` since the rename) and installed
whatever mockgen happened to be on PATH. Run it through `go run` with a
pinned version so regeneration is reproducible; v0.4.0 is the last
version usable here, as the package mode introduced in v0.5 copies
parameter names from the mocked source and moby's APIClient names its
variadic options `_`, which produces invalid Go code.
Regenerated pkg/mocks with the fixed invocation (header-only change).
Signed-off-by: Nicolas De Loof <[email protected]>
The display.Mode package global was left as "auto" on the most common
path (auto mode with an interactive or redirected terminal), so readers
like `up`'s NavigationMenu gate saw an unresolved value; that gate also
compared against the string literal "plain" instead of the constant.
applyDisplayMode wrote a mode that selectEventProcessor could then
contradict (--ansi always with a redirected stderr rendered Plain while
Mode said tty).
selectEventProcessor becomes the single resolution point: every branch
assigns the mode matching the renderer it returns, so after command
setup the global never holds ModeAuto. applyDisplayMode loses its
redundant writes and its misleading name (now applyAnsiMode); the
contract is documented on the Mode variable, and the existing
selectEventProcessor tests now pin the resolved mode for every branch.
Side effect, deliberate: with stderr redirected (auto -> plain
rendering) the navigation menu is no longer offered; it used to slip
through because Mode still read "auto".
Part of #14074 (section E).
Signed-off-by: Nicolas De Loof <[email protected]>
The imperative start engine (startService, startServiceContainer,
restart, waitDependencies) is only locked by e2e tests today. Before
converging it into the plan engine (#14081), pin its observable
behavior with unit characterization tests: start idempotence and the
silent no-op on running replicas, the "no container to start" error,
pre_start gating (mixed replicas skip the hook, all-stopped runs it
once on the lowest-numbered replica), the inject -> start -> post_start
sequence with Started only emitted after hooks, restart's depends_on
restart:true selection and its pre_stop -> ContainerRestart ->
post_start order, --wait's completed_successfully condition mapping,
and waitDependencies' missing/failing dependency semantics for
required and optional dependencies.
No production change.
Part of #14074 (section C) / #14081 (lot 0).
Signed-off-by: Nicolas De Loof <[email protected]>
The --format flag defaulted to "table" instead of "", so the
opts.Format == "" guard in runPs never fired for a bare compose ps.
Setting the default to "" lets the existing config-fallback path work,
matching the behaviour of docker ps.
Fixes#13643
Supersedes #14032
Signed-off-by: Guillaume Lours <[email protected]>
createNetwork built the IPAM object in two separate blocks; the second
silently overwrote createOpts.IPAM with a fresh empty struct and never
forwarded n.Ipam.Options to the daemon. Consolidate into a single block
covering driver, config pool, and options.
Fixes#13785
Supersedes #13936
Signed-off-by: Guillaume Lours <[email protected]>
RunOptions embeds CreateOptions, which suggests every creation option
applies to the one-off's dependencies; in reality only Build,
IgnoreOrphans, RemoveOrphans and QuietPull are propagated
(startDependencies builds a fresh CreateOptions from exactly those
four). Setting Recreate or Inherit on a RunOptions was a silent no-op.
The same struct also serves Exec, which honors only the exec-relevant
subset of its ~20 fields.
Both facts are now stated on the type, the embedding and the
Exec-only Index field.
Part of #14074 (B: pkg/api promises things the implementation does not
honor).
Signed-off-by: Nicolas De Loof <[email protected]>
StartOptions fuses two contracts: the fields s.start honors (Project,
AttachTo-as-project-source, Wait, WaitTimeout) and the fields only Up's
foreground session reads (Attach, OnExit, ExitCodeFrom, Watch,
NavigationMenu). Passing the latter to Start compiled, returned no
error, and did nothing.
Each session field is now marked 'honored by Up's foreground session
only; ignored by Start'. Two implicit behaviors become explicit along
the way: Attach doubles as the detached/interactive mode switch (nil
means Up returns once containers are started), and AttachTo has two
unrelated meanings (project reconstruction source for Start, log
scoping for Up).
Part of #14074 (B: pkg/api promises things the implementation does not
honor).
Signed-off-by: Nicolas De Loof <[email protected]>
Three structs carry a Services field with three different meanings:
ProjectLoadOptions.Services narrows the loaded project (the only real
filter), CreateOptions.Services only selects which recreation policy
applies to which service while the WHOLE project converges, and
StartOptions.Services only scopes Up's log monitor and is ignored by
Start. Nothing said so: an SDK consumer calling
Create(project, CreateOptions{Services: []string{"web"}}) reasonably
expected to create web alone, and got the full project.
The interface doc now states the underlying invariant — methods act on
the whole project they receive; scoping is done by narrowing the
project, and downstream Services fields are intent markers, not
filters — and each of the three fields documents its actual effect.
Part of #14074 (B: pkg/api promises things the implementation does not
honor).
Signed-off-by: Nicolas De Loof <[email protected]>
initialSyncFiles and initialSyncDirectory filtered out any host file
whose mtime predated the image creation time — which is exactly the
files that initial_sync is meant to copy. The imageCreatedTime lookup
(ContainerList + ImageInspect) was also a silent failure point when the
container list returned nothing. Remove the filter and the helper
entirely: sync all files that are not ignored or bind-mounted.
Fixes#13725
Supersedes #14031
Signed-off-by: Guillaume Lours <[email protected]>