From 05d38eda685df6e666382ec5fa12b11a37d800af Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 14:10:51 +0200 Subject: [PATCH 1/6] chore(compose): rename convergence.go, whose convergence engine no longer exists 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 --- pkg/compose/create.go | 2 +- .../{convergence.go => service_containers.go} | 16 +++++++++++----- ...rgence_test.go => service_containers_test.go} | 0 3 files changed, 12 insertions(+), 6 deletions(-) rename pkg/compose/{convergence.go => service_containers.go} (96%) rename pkg/compose/{convergence_test.go => service_containers_test.go} (100%) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index c52c2e5863a..ecc48d1333f 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -659,7 +659,7 @@ func defaultNetworkSettings(project *types.Project, // in the network configuration instead of connecting the container to each extra // network individually after creation. // For older API versions, extra networks are connected via NetworkConnect after - // container creation (see createMobyContainer in convergence.go). + // container creation (see createMobyContainer in service_containers.go). if !versions.LessThan(version, apiVersion144) { for _, networkKey := range serviceNetworks { epSettings, err := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases) diff --git a/pkg/compose/convergence.go b/pkg/compose/service_containers.go similarity index 96% rename from pkg/compose/convergence.go rename to pkg/compose/service_containers.go index a5e0c146fea..4e49fb39883 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/service_containers.go @@ -38,17 +38,23 @@ import ( "github.com/docker/compose/v5/pkg/api" ) +// This file gathers the per-service container helpers shared by both +// lifecycle engines: the plan-based reconciler (reconcile.go, entered through +// create/up) and the imperative dependency-ordered engine (dependencies.go, +// used by start/stop/restart/down). It covers container naming, resolution of +// service references (volumes_from, network_mode/ipc/pid, links), dependency +// waiting, container creation through the Docker API, and service startup. + const ( doubledContainerNameWarning = "WARNING: The %q service is using the custom container name %q. " + "Docker requires each container to have a unique name. " + "Remove the custom name to scale the service" ) -// convergence manages service's container lifecycle. -// Based on initially observed state, it reconciles the existing container with desired state, which might include -// re-creating container, adding or removing replicas, or starting stopped containers. -// Cross services dependencies are managed by creating services in expected order and updating `service:xx` reference -// when a service has converged, so dependent ones can be managed with resolved containers references. +// getScale returns the number of replicas the service must run. A service +// pinned to a custom container_name cannot scale beyond one replica, as every +// container needs a distinct name: this is rejected here rather than at +// container-creation time. func getScale(config types.ServiceConfig) (int, error) { scale := config.GetScale() if scale > 1 && config.ContainerName != "" { diff --git a/pkg/compose/convergence_test.go b/pkg/compose/service_containers_test.go similarity index 100% rename from pkg/compose/convergence_test.go rename to pkg/compose/service_containers_test.go From 3026da004021940739d941709a80a581f7cd120b Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 14:11:59 +0200 Subject: [PATCH 2/6] chore(compose): comments state behavior, not history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/compose/create.go | 8 +++---- pkg/compose/observed_state.go | 10 ++++---- pkg/compose/reconcile.go | 45 ++++++++++++++++++++++------------- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index ecc48d1333f..b0f60ce30de 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -124,8 +124,8 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt return err } - // Emit "Running" events for containers that are already up-to-date, - // matching the previous convergence behavior for progress display. + // Emit "Running" events for containers that are already up-to-date, so + // the progress display accounts for containers the plan will not touch. emitRunningEvents(project, observed, plan, s.events) return s.executePlan(ctx, project, observed, plan) @@ -1461,8 +1461,8 @@ func (s *composeService) createNetwork(ctx context.Context, n *types.NetworkConf if _, err := s.apiClient().NetworkCreate(ctx, n.Name, networkCreateOptions); err != nil { // A concurrent `docker compose up|run` may have created the same network // between the observed-state snapshot and now. Treat the resulting - // conflict as success rather than failing hard, mirroring the retry the - // previous ensureNetwork performed. + // conflict as success rather than failing hard: the network we wanted + // exists. if errdefs.IsConflict(err) { s.events.On(createdEvent(networkEventName)) return nil diff --git a/pkg/compose/observed_state.go b/pkg/compose/observed_state.go index c2ade89db88..cfef7f3718e 100644 --- a/pkg/compose/observed_state.go +++ b/pkg/compose/observed_state.go @@ -151,8 +151,9 @@ func (s *composeService) collectObservedState(ctx context.Context, project *type } // --- Containers --- - // Use oneOffInclude to detect orphaned one-off containers (matching the - // previous behavior of create() which used oneOffInclude + isOrphaned). + // One-off (run) containers are included in the listing on purpose: exited + // one-offs are classified as orphans below, so `--remove-orphans` can + // clean them up. raw, err := s.getContainers(ctx, project.Name, oneOffInclude, true) if err != nil { return nil, err @@ -365,8 +366,9 @@ func (s *ObservedState) setResolvedVolumes(volumes map[string]string) { } // emitRunningEvents emits "Running" progress events for containers that are already -// running and have no operations planned for them. This matches the previous behavior -// where convergence.ensureService emitted runningEvent for up-to-date containers. +// running and have no operations planned for them, so the progress display +// accounts for every container of the project — including the up-to-date ones +// the plan deliberately leaves alone. // // Iterates project.Services (not observed.Containers) so that containers of // disabled services (e.g. dependencies untouched by `compose run --no-deps`) diff --git a/pkg/compose/reconcile.go b/pkg/compose/reconcile.go index 752e303ba39..1d6b7f98e50 100644 --- a/pkg/compose/reconcile.go +++ b/pkg/compose/reconcile.go @@ -192,7 +192,8 @@ func (r *reconciler) resolveObserved() { // Divergence is detected by comparing NetworkHash(desired) with the config-hash // persisted on the live network (observed.ConfigHash). A network with no // recorded hash (e.g. created by an older Compose or manually) is left -// untouched, matching the previous ensureNetwork behavior. +// untouched: without a recorded hash there is no reliable way to tell +// configuration drift apart from deliberate manual setup. // // A rename (observed.Name != desired.Name) also diverges the hash — NetworkHash // includes the name — and is handled by the same recreation path: the old @@ -242,7 +243,8 @@ func (r *reconciler) planCreateNetwork(key string, networkConfig *types.NetworkC // Attached containers must be disconnected before the network can be removed // (Docker refuses to remove a network with active endpoints) and are reconnected // to the fresh network afterwards — they keep their identity and are not -// recreated, matching the previous ensureNetwork/removeDivergedNetwork behavior. +// recreated: a network definition change alone is not a reason to lose +// container state. // // Stops are deduplicated through stoppedByPlan so a container attached to several // diverged networks (or later recreated by reconcileContainers) is stopped once. @@ -345,8 +347,9 @@ func (r *reconciler) planRecreateNetworks(keys []string) { // // Divergence is detected by comparing VolumeHash(desired) with the config-hash // persisted on the live volume (observed.ConfigHash). A volume with no recorded -// hash (e.g. created by an older Compose) is left untouched, matching the -// previous ensureVolume behavior. +// hash (e.g. created by an older Compose) is left untouched: without a +// recorded hash there is no reliable way to tell configuration drift apart +// from deliberate manual setup, and volumes carry data. func (r *reconciler) reconcileVolumes() error { var diverged []string for _, key := range sortedKeys(r.project.Volumes) { @@ -368,16 +371,16 @@ func (r *reconciler) reconcileVolumes() error { } if observed.Name != desired.Name { // The volume was renamed: the live volume matched by label carries a - // different name, i.e. a distinct Docker resource. Match the - // historical additive behavior — create the new volume and leave the - // old one (and its data) untouched — instead of prompting to delete - // data under a name that does not exist yet. + // different name, i.e. a distinct Docker resource. A rename is + // additive — create the new volume and leave the old one (and its + // data) untouched — instead of prompting to delete data under a + // name that does not exist yet. r.planCreateVolume(key, &desired, "renamed") // Rewrite the observed name to the desired one so reconcileContainers // detects the mount mismatch and migrates existing containers onto - // the new volume within the same up (as the pre-reconcile ensureVolume - // path did), and so later runs match deterministically on the new - // name rather than split-braining between the two. + // the new volume within the same up, and so later runs match + // deterministically on the new name rather than split-braining + // between the two. observed.Name = desired.Name r.resolvedVolumes[key] = observed continue @@ -676,8 +679,9 @@ func (r *reconciler) reconcileService(service types.ServiceConfig) error { } parentRecreated := r.parentNamespaceRecreated(service) - // Sort containers: obsolete first, then by number descending, then reverse - // to get the same ordering as the existing convergence code. + // Order containers so that the scale-down below (i >= expected) trims + // obsolete containers and the highest replica numbers first — see + // sortContainers for the resulting ordering. r.sortContainers(containers, service, expectedHash, parentRecreated, strategy) // Collect dependency nodes that container creation should depend on @@ -716,7 +720,11 @@ func (r *reconciler) reconcileService(service types.ServiceConfig) error { // Container is up-to-date switch oc.State { case container.StateRunning, container.StateCreated, container.StateRestarting, container.StateExited: - // Nothing to do (exited containers are left as-is, matching convergence.go behavior) + // Nothing to plan. Starting created/exited containers is NOT the + // plan's job: `up` runs a separate start phase afterwards + // (start.go), which lists containers again and starts them in + // dependency order. Exited containers are deliberately left as-is + // here so that phase (or the user) decides. default: // Any other state (paused, dead, ...): attempt to (re)start lastNode = r.plan.addNode(Operation{ @@ -1009,8 +1017,13 @@ func (r *reconciler) infrastructureDeps(service types.ServiceConfig) []*PlanNode return deps } -// sortContainers sorts containers the same way as convergence.go:138-160: -// obsolete first, then by container number descending, then reversed. +// sortContainers orders the slice so that, read from the front, up-to-date +// containers come first in ascending replica-number order, followed by the +// containers that must be recreated (the comparator sorts obsolete-first and +// number-descending, then the slice is reversed). Scale-down trims the tail +// of this slice (i >= expected in reconcileService), so this ordering is what +// guarantees that obsolete containers and the highest replica numbers are +// removed first while low-numbered healthy replicas survive. // // mustRecreate is evaluated once per container before sorting to avoid // quadratic re-evaluation in the comparator. From 67e194ee7be295e17bc68d0568bbb70d70618fc5 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 14:12:30 +0200 Subject: [PATCH 3/6] docs(sdk): document the EventProcessor API that actually exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/sdk.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/sdk.md b/docs/sdk.md index 3a03c7fdc1d..a18e2c681ac 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -107,7 +107,7 @@ options allow you to configure I/O streams, concurrency limits, dry-run mode, an - `WithDryRun` - Run operations in dry-run mode without actually applying changes - `WithContextInfo(api.ContextInfo)` - Set custom Docker context information - `WithProxyConfig(map[string]string)` - Configure HTTP proxy settings for builds -- `WithEventProcessor(progress.EventProcessor)` - Receive progress events and operation notifications +- `WithEventProcessor(api.EventProcessor)` - Receive progress events and operation notifications These options provide fine-grained control over the SDK's behavior, making it suitable for various integration scenarios including CLI tools, web services, automation scripts, and testing environments. @@ -145,13 +145,17 @@ Common status text values include: `Creating`, `Created`, `Starting`, `Started`, ### Built-in `EventProcessor` implementations -The SDK provides three ready-to-use `EventProcessor` implementations: +The `EventProcessor` interface is defined in `github.com/docker/compose/v5/pkg/api`. When no +`WithEventProcessor` option is passed, events are silently discarded. -- `progress.NewTTYWriter(io.Writer)` - Renders an interactive terminal UI with progress bars and task lists - (similar to the Docker Compose CLI output) -- `progress.NewPlainWriter(io.Writer)` - Outputs simple text-based progress messages suitable for non-interactive +The renderers used by the Docker Compose CLI live in the `github.com/docker/compose/v5/cmd/display` +package and can be reused: + +- `display.Full(out, info io.Writer, detached bool)` - Renders the interactive terminal UI with progress bars + and task lists (the default Docker Compose CLI output) +- `display.Plain(out io.Writer)` - Outputs simple text-based progress messages suitable for non-interactive environments or log files -- `progress.NewJSONWriter()` - Render events as JSON objects -- `progress.NewQuietWriter()` - (Default) Silently processes events without producing any output +- `display.JSON(out io.Writer)` - Renders each event as a JSON object +- `display.Quiet()` - Silently discards events (same behavior as the default) Using `EventProcessor`, a custom UI can be plugged into `docker/compose`. From 7a414a522085670843e556acf202a70a61e1eb19 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 14:13:47 +0200 Subject: [PATCH 4/6] test(e2e): gate the e2e suite behind the build tag the docs already promise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 6 ++++-- Makefile | 4 ++-- pkg/e2e/bridge_test.go | 2 ++ pkg/e2e/build_test.go | 2 ++ pkg/e2e/cancel_test.go | 2 +- pkg/e2e/cascade_test.go | 2 +- pkg/e2e/commit_test.go | 2 ++ pkg/e2e/compose_environment_test.go | 2 ++ pkg/e2e/compose_exec_test.go | 2 ++ pkg/e2e/compose_run_build_once_test.go | 2 ++ pkg/e2e/compose_run_test.go | 2 ++ pkg/e2e/compose_test.go | 2 ++ pkg/e2e/compose_up_test.go | 2 ++ pkg/e2e/config_test.go | 2 ++ pkg/e2e/configs_test.go | 2 ++ pkg/e2e/container_name_test.go | 2 +- pkg/e2e/cp_test.go | 2 ++ pkg/e2e/env_file_test.go | 2 ++ pkg/e2e/events_test.go | 2 ++ pkg/e2e/exec_test.go | 2 ++ pkg/e2e/export_test.go | 2 ++ pkg/e2e/expose_test.go | 2 +- pkg/e2e/healthcheck_test.go | 2 ++ pkg/e2e/hooks_test.go | 2 ++ pkg/e2e/image_identity_corner_test.go | 2 ++ pkg/e2e/image_identity_test.go | 2 ++ pkg/e2e/images_test.go | 2 ++ pkg/e2e/ipc_test.go | 2 ++ pkg/e2e/logs_test.go | 2 ++ pkg/e2e/main_test.go | 2 ++ pkg/e2e/model_test.go | 2 ++ pkg/e2e/multiplatform_test.go | 2 ++ pkg/e2e/networks_test.go | 2 ++ pkg/e2e/noDeps_test.go | 2 +- pkg/e2e/orphans_test.go | 2 ++ pkg/e2e/pause_test.go | 2 ++ pkg/e2e/profiles_test.go | 2 ++ pkg/e2e/providers_test.go | 2 ++ pkg/e2e/ps_test.go | 2 ++ pkg/e2e/publish_test.go | 2 ++ pkg/e2e/pull_test.go | 2 ++ pkg/e2e/recreate_no_deps_test.go | 2 ++ pkg/e2e/restart_test.go | 2 ++ pkg/e2e/scale_test.go | 2 ++ pkg/e2e/secrets_test.go | 2 ++ pkg/e2e/start_stop_test.go | 2 ++ pkg/e2e/testdata_test.go | 2 ++ pkg/e2e/up_test.go | 2 +- pkg/e2e/volumes_test.go | 2 ++ pkg/e2e/wait_test.go | 2 ++ pkg/e2e/watch_test.go | 2 ++ 51 files changed, 98 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e91c95f52ae..ab2ff6128d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,11 @@ - Build: `make build` - Test all: `make test` -- Test unit: `go test ./pkg/...` +- Test unit: `go test ./pkg/...` — needs no Docker daemon; the e2e suite is + gated behind the `e2e` build tag and is not picked up - Test single: `go test ./pkg/compose/ -run TestFunctionName` -- E2E tests: `go test -tags e2e ./pkg/e2e/ -run TestName` +- E2E tests: `go test -tags e2e ./pkg/e2e/ -run TestName` — requires a Docker + daemon and the locally built binary (`make build`) ## E2E tests diff --git a/Makefile b/Makefile index ee295c55243..85816f97960 100644 --- a/Makefile +++ b/Makefile @@ -77,11 +77,11 @@ install: binary .PHONY: e2e-compose e2e-compose: example-provider ## Run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test - go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 -parallel=$(E2E_PARALLEL_PLUGIN) -timeout 20m ./pkg/e2e + go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- -v $(TEST_FLAGS) -count=1 -parallel=$(E2E_PARALLEL_PLUGIN) -timeout 20m -tags e2e ./pkg/e2e .PHONY: e2e-compose-standalone e2e-compose-standalone: ## Run End to end local tests in standalone mode. Set E2E_TEST=TestName to run a single test - go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=$(E2E_STANDALONE_PARALLEL) -timeout 20m --tags=standalone ./pkg/e2e + go run gotest.tools/gotestsum@latest --format testname --junitfile "/tmp/report/report.xml" -- $(TEST_FLAGS) -v -count=1 -parallel=$(E2E_STANDALONE_PARALLEL) -timeout 20m --tags=e2e,standalone ./pkg/e2e .PHONY: build-and-e2e-compose build-and-e2e-compose: build e2e-compose ## Compile the compose cli-plugin and run end to end local tests in plugin mode. Set E2E_TEST=TestName to run a single test diff --git a/pkg/e2e/bridge_test.go b/pkg/e2e/bridge_test.go index 7212bcaf2c0..19f8eec4788 100644 --- a/pkg/e2e/bridge_test.go +++ b/pkg/e2e/bridge_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 25fd08b4fd3..1b58c074aeb 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/cancel_test.go b/pkg/e2e/cancel_test.go index 64f3ff609a9..4802d64abbc 100644 --- a/pkg/e2e/cancel_test.go +++ b/pkg/e2e/cancel_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build e2e && !windows /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/cascade_test.go b/pkg/e2e/cascade_test.go index a201acc2b31..f9f72ca1ef9 100644 --- a/pkg/e2e/cascade_test.go +++ b/pkg/e2e/cascade_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build e2e && !windows /* Copyright 2022 Docker Compose CLI authors diff --git a/pkg/e2e/commit_test.go b/pkg/e2e/commit_test.go index 7298b6e7985..c0e3217ac5f 100644 --- a/pkg/e2e/commit_test.go +++ b/pkg/e2e/commit_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2023 Docker Compose CLI authors diff --git a/pkg/e2e/compose_environment_test.go b/pkg/e2e/compose_environment_test.go index 36fa855042e..447104c963d 100644 --- a/pkg/e2e/compose_environment_test.go +++ b/pkg/e2e/compose_environment_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/compose_exec_test.go b/pkg/e2e/compose_exec_test.go index 9e58d566842..b5cf163da93 100644 --- a/pkg/e2e/compose_exec_test.go +++ b/pkg/e2e/compose_exec_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/compose_run_build_once_test.go b/pkg/e2e/compose_run_build_once_test.go index 31b124665c3..608adfe1278 100644 --- a/pkg/e2e/compose_run_build_once_test.go +++ b/pkg/e2e/compose_run_build_once_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/compose_run_test.go b/pkg/e2e/compose_run_test.go index 3a80fdc86bb..31feafe504e 100644 --- a/pkg/e2e/compose_run_test.go +++ b/pkg/e2e/compose_run_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index f0ff1da0707..59d6cdd481a 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/compose_up_test.go b/pkg/e2e/compose_up_test.go index 1acaf7393d4..35c2f5c57bc 100644 --- a/pkg/e2e/compose_up_test.go +++ b/pkg/e2e/compose_up_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/config_test.go b/pkg/e2e/config_test.go index 585ee7419fc..0be97c28ebd 100644 --- a/pkg/e2e/config_test.go +++ b/pkg/e2e/config_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/configs_test.go b/pkg/e2e/configs_test.go index 899c5168ff3..05b2c99bf62 100644 --- a/pkg/e2e/configs_test.go +++ b/pkg/e2e/configs_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/container_name_test.go b/pkg/e2e/container_name_test.go index 79332ab6ac8..2f0fac79000 100644 --- a/pkg/e2e/container_name_test.go +++ b/pkg/e2e/container_name_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build e2e && !windows /* Copyright 2022 Docker Compose CLI authors diff --git a/pkg/e2e/cp_test.go b/pkg/e2e/cp_test.go index e9ea7691ad4..5847af4e96e 100644 --- a/pkg/e2e/cp_test.go +++ b/pkg/e2e/cp_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/env_file_test.go b/pkg/e2e/env_file_test.go index 5a5a9643065..c7ae466dcb4 100644 --- a/pkg/e2e/env_file_test.go +++ b/pkg/e2e/env_file_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/events_test.go b/pkg/e2e/events_test.go index 1c9b19334a4..ef20d7264c1 100644 --- a/pkg/e2e/events_test.go +++ b/pkg/e2e/events_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/exec_test.go b/pkg/e2e/exec_test.go index 1f8d1018c0b..d5e70087f4b 100644 --- a/pkg/e2e/exec_test.go +++ b/pkg/e2e/exec_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2023 Docker Compose CLI authors diff --git a/pkg/e2e/export_test.go b/pkg/e2e/export_test.go index 5cceb693a18..12444880062 100644 --- a/pkg/e2e/export_test.go +++ b/pkg/e2e/export_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2023 Docker Compose CLI authors diff --git a/pkg/e2e/expose_test.go b/pkg/e2e/expose_test.go index f7e7d3c2f4a..0fb7d4d15db 100644 --- a/pkg/e2e/expose_test.go +++ b/pkg/e2e/expose_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build e2e && !windows /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/healthcheck_test.go b/pkg/e2e/healthcheck_test.go index 2cb42fd54a5..b25376f9cc3 100644 --- a/pkg/e2e/healthcheck_test.go +++ b/pkg/e2e/healthcheck_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2023 Docker Compose CLI authors diff --git a/pkg/e2e/hooks_test.go b/pkg/e2e/hooks_test.go index 8d44de83801..e7db829f33a 100644 --- a/pkg/e2e/hooks_test.go +++ b/pkg/e2e/hooks_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2023 Docker Compose CLI authors diff --git a/pkg/e2e/image_identity_corner_test.go b/pkg/e2e/image_identity_corner_test.go index 810304219af..4f2d3154ffa 100644 --- a/pkg/e2e/image_identity_corner_test.go +++ b/pkg/e2e/image_identity_corner_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2026 Docker Compose CLI authors diff --git a/pkg/e2e/image_identity_test.go b/pkg/e2e/image_identity_test.go index dd1a9b01f9f..b2e881c15ff 100644 --- a/pkg/e2e/image_identity_test.go +++ b/pkg/e2e/image_identity_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/images_test.go b/pkg/e2e/images_test.go index 8511d7ff85a..5ffdf3a1097 100644 --- a/pkg/e2e/images_test.go +++ b/pkg/e2e/images_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2026 Docker Compose CLI authors diff --git a/pkg/e2e/ipc_test.go b/pkg/e2e/ipc_test.go index 010854b60ea..63dcddb9558 100644 --- a/pkg/e2e/ipc_test.go +++ b/pkg/e2e/ipc_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/logs_test.go b/pkg/e2e/logs_test.go index afc2e089081..493385dae88 100644 --- a/pkg/e2e/logs_test.go +++ b/pkg/e2e/logs_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/main_test.go b/pkg/e2e/main_test.go index 415987bc71b..9a710c7a9cd 100644 --- a/pkg/e2e/main_test.go +++ b/pkg/e2e/main_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/model_test.go b/pkg/e2e/model_test.go index a9e6d3d6fd1..baeb6c3d24a 100644 --- a/pkg/e2e/model_test.go +++ b/pkg/e2e/model_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/multiplatform_test.go b/pkg/e2e/multiplatform_test.go index 651c51519b1..2590a43408b 100644 --- a/pkg/e2e/multiplatform_test.go +++ b/pkg/e2e/multiplatform_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2026 Docker Compose CLI authors diff --git a/pkg/e2e/networks_test.go b/pkg/e2e/networks_test.go index 438e31eb284..462ccb35d73 100644 --- a/pkg/e2e/networks_test.go +++ b/pkg/e2e/networks_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/noDeps_test.go b/pkg/e2e/noDeps_test.go index deacbf023bd..f1ad0be2c93 100644 --- a/pkg/e2e/noDeps_test.go +++ b/pkg/e2e/noDeps_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build e2e && !windows /* Copyright 2022 Docker Compose CLI authors diff --git a/pkg/e2e/orphans_test.go b/pkg/e2e/orphans_test.go index 0e12a494a1e..77eff1562c5 100644 --- a/pkg/e2e/orphans_test.go +++ b/pkg/e2e/orphans_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/pause_test.go b/pkg/e2e/pause_test.go index d8b037c26ca..0fa4489138c 100644 --- a/pkg/e2e/pause_test.go +++ b/pkg/e2e/pause_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/profiles_test.go b/pkg/e2e/profiles_test.go index 6c1256a05f4..0a2883aa848 100644 --- a/pkg/e2e/profiles_test.go +++ b/pkg/e2e/profiles_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/providers_test.go b/pkg/e2e/providers_test.go index 91b0ab04504..0b8031c3f4a 100644 --- a/pkg/e2e/providers_test.go +++ b/pkg/e2e/providers_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/ps_test.go b/pkg/e2e/ps_test.go index 8ea47f7463b..2aae0a53ae1 100644 --- a/pkg/e2e/ps_test.go +++ b/pkg/e2e/ps_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/publish_test.go b/pkg/e2e/publish_test.go index e8c46c7d0ab..902c5422151 100644 --- a/pkg/e2e/publish_test.go +++ b/pkg/e2e/publish_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/pull_test.go b/pkg/e2e/pull_test.go index 979b08de42a..906fff4f674 100644 --- a/pkg/e2e/pull_test.go +++ b/pkg/e2e/pull_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/recreate_no_deps_test.go b/pkg/e2e/recreate_no_deps_test.go index 8ba94b74eba..e9a5b0267c5 100644 --- a/pkg/e2e/recreate_no_deps_test.go +++ b/pkg/e2e/recreate_no_deps_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2022 Docker Compose CLI authors diff --git a/pkg/e2e/restart_test.go b/pkg/e2e/restart_test.go index 21746e0dce0..6f0e316b6ab 100644 --- a/pkg/e2e/restart_test.go +++ b/pkg/e2e/restart_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/scale_test.go b/pkg/e2e/scale_test.go index 92e135075ee..9184d03a498 100644 --- a/pkg/e2e/scale_test.go +++ b/pkg/e2e/scale_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/secrets_test.go b/pkg/e2e/secrets_test.go index 3147c3765b5..78ead037d77 100644 --- a/pkg/e2e/secrets_test.go +++ b/pkg/e2e/secrets_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index bad7fdb4a2e..5bf1f3e589d 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/testdata_test.go b/pkg/e2e/testdata_test.go index 780fa27827c..b0e43b45d70 100644 --- a/pkg/e2e/testdata_test.go +++ b/pkg/e2e/testdata_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/up_test.go b/pkg/e2e/up_test.go index c4d0856a14f..01c11b7b932 100644 --- a/pkg/e2e/up_test.go +++ b/pkg/e2e/up_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build e2e && !windows /* Copyright 2022 Docker Compose CLI authors diff --git a/pkg/e2e/volumes_test.go b/pkg/e2e/volumes_test.go index 0407f016a3f..1827d9fbba7 100644 --- a/pkg/e2e/volumes_test.go +++ b/pkg/e2e/volumes_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/wait_test.go b/pkg/e2e/wait_test.go index 23a80020d14..b8fb8d86d7a 100644 --- a/pkg/e2e/wait_test.go +++ b/pkg/e2e/wait_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2020 Docker Compose CLI authors diff --git a/pkg/e2e/watch_test.go b/pkg/e2e/watch_test.go index 225d7fa065e..7267918505c 100644 --- a/pkg/e2e/watch_test.go +++ b/pkg/e2e/watch_test.go @@ -1,3 +1,5 @@ +//go:build e2e + /* Copyright 2023 Docker Compose CLI authors From 2ec3b705717e8f388a27b4a08dfa65f65588e167 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 14:14:39 +0200 Subject: [PATCH 5/6] build: error messages name the real remedy, not a removed builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/compose/build_bake.go | 3 ++- pkg/compose/build_classic.go | 10 +++++----- pkg/e2e/build_test.go | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/compose/build_bake.go b/pkg/compose/build_bake.go index 31e2a58c5db..29520d91160 100644 --- a/pkg/compose/build_bake.go +++ b/pkg/compose/build_bake.go @@ -64,7 +64,8 @@ func buildWithBake(dockerCli command.Cli) (bool, error) { _, err = manager.GetPlugin("buildx", dockerCli, &cobra.Command{}) if err != nil { if errdefs.IsNotFound(err) { - logrus.Warnf("Docker Compose requires buildx plugin to be installed") + logrus.Warnf("buildx Docker CLI plugin not found: falling back to the classic builder. " + + "BuildKit-only build features (multi-arch, secrets, ssh, additional contexts, ...) will not be available") return false, nil } return false, err diff --git a/pkg/compose/build_classic.go b/pkg/compose/build_classic.go index af94121f568..3e7afa2432e 100644 --- a/pkg/compose/build_classic.go +++ b/pkg/compose/build_classic.go @@ -232,19 +232,19 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec // doesn't implement func checkClassicBuilderSupported(service types.ServiceConfig) error { if len(service.Build.Platforms) > 1 { - return fmt.Errorf("the classic builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use BuildKit") + return fmt.Errorf("the classic builder doesn't support multi-arch build; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0") } if service.Build.Privileged { - return fmt.Errorf("the classic builder doesn't support privileged mode, set DOCKER_BUILDKIT=1 to use BuildKit") + return fmt.Errorf("the classic builder doesn't support privileged mode; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0") } if len(service.Build.AdditionalContexts) > 0 { - return fmt.Errorf("the classic builder doesn't support additional contexts, set DOCKER_BUILDKIT=1 to use BuildKit") + return fmt.Errorf("the classic builder doesn't support additional contexts; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0") } if len(service.Build.SSH) > 0 { - return fmt.Errorf("the classic builder doesn't support SSH keys, set DOCKER_BUILDKIT=1 to use BuildKit") + return fmt.Errorf("the classic builder doesn't support SSH keys; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0") } if len(service.Build.Secrets) > 0 { - return fmt.Errorf("the classic builder doesn't support secrets, set DOCKER_BUILDKIT=1 to use BuildKit") + return fmt.Errorf("the classic builder doesn't support secrets; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0") } return nil } diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 1b58c074aeb..3992508946b 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -404,7 +404,7 @@ func TestBuildPlatformsStandardErrors(t *testing.T) { }) res.Assert(t, icmd.Expected{ ExitCode: 1, - Err: "the classic builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use BuildKit", + Err: "the classic builder doesn't support multi-arch build; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0", }) }) @@ -472,7 +472,7 @@ func TestBuildPlatformsStandardErrors(t *testing.T) { }) res.Assert(t, icmd.Expected{ ExitCode: 1, - Err: "the classic builder doesn't support privileged mode, set DOCKER_BUILDKIT=1 to use BuildKit", + Err: "the classic builder doesn't support privileged mode; building with BuildKit requires the buildx Docker CLI plugin, and DOCKER_BUILDKIT must not be set to 0", }) }) } From f744a809577d0f88e9229ee8981d047977669fb7 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 17 Aug 2026 14:15:47 +0200 Subject: [PATCH 6/6] docs(agents): add an architecture map of the non-guessable facts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md covered build/test/lint mechanics but nothing about how the codebase is organized, so a reader's first mental model comes from file names and local comments — several of which used to be misleading (see the previous commits in this series). The new section states, in ~35 lines, the facts a contributor cannot derive from local reading: the two coexisting lifecycle engines and which commands use which (including that the plan never starts containers), the Run()/EventProcessor operation convention, the stdout/stderr split, the two environment-resolution idioms and the setEnvWithDotEnv timing, the config-hash label as the actual project membership invariant, and the Docker Desktop integration points that must fail silently. Part of #14074 (A: the code misdescribes its own structure). Signed-off-by: Nicolas De Loof --- AGENTS.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ab2ff6128d0..d30e38bb847 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,49 @@ `OutputContains` as last resort, new checks go in `pkg/e2e/checks.go`) and how to exploit failure artifacts and `E2E_KEEP_FAILED=1`. +## Architecture map + +Read this before changing lifecycle or CLI code — several of these facts are +not guessable from local reading. + +- **Packages**: `cmd/compose` is the cobra CLI layer; `pkg/api` is the public + SDK contract (interface `Compose` + option structs); `pkg/compose` is the + backend implementation (`NewComposeService`). Everything else under `pkg/` + is exported for historical reasons, not as a stability promise. +- **Two lifecycle engines coexist.** The plan-based reconciler + (`pkg/compose/reconcile.go`) has a single entry point: `create()` — so only + `create`/`up`/`run`/`scale`/`watch` go through it. `start`, `stop`, + `restart` and `down` use the imperative dependency-ordered engine + (`pkg/compose/dependencies.go`) with the shared helpers in + `service_containers.go`. The plan does **not** start containers: `up` runs + a separate start phase (`start.go`) that re-lists containers and starts + them in dependency order. +- **Operation convention**: exported `composeService` methods wrap the work + in `Run(ctx, …, "opname")`, which drives the `EventProcessor` + (`Start`/`On`/`Done`) used for progress display. Unexported methods + (`s.create`, `s.start`, …) are building blocks without their own event + cycle — calling an exported method from another operation nests event + cycles. +- **Output conventions**: progress/status rendering goes to **stderr** + (`dockerCli.Err()`); stdout is reserved for command payload (`ps`, + `config`, container logs' stdout stream). TTY detection for the renderer + probes stderr, color detection for logs probes stdout. +- **Environment resolution has two idioms**: `os.Getenv` sees only the + process environment; `project.Environment[...]` also sees the project's + `.env`. `cmd/compose/compose.go:setEnvWithDotEnv` additionally copies + `COMPOSE_*` keys from the project's env-files into the process environment + during `PersistentPreRunE` — variables read before that point (e.g. as + cobra flag defaults) do not see the `.env`. +- **Container/project identity is label-based**: listings filter on the + presence of `com.docker.compose.config-hash` (see + `pkg/compose/containers.go:getDefaultFilters`), not just the project label. + One-off (`compose run`) containers carry `oneoff=True` and no + container-number; lifecycle-hook helper containers carry neither and are + therefore invisible to `ps`/`down`. +- **Docker Desktop integrations** (`internal/desktop`, used from `up`, + `publish`, the keyboard menu) talk to Desktop over a private socket and are + expected to fail silently on non-Desktop engines. + ## Lint - Linter: golangci-lint v2 (config in `.golangci.yml`)