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 c52c2e5863..ecc48d1333 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 a5e0c146fe..4e49fb3988 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 ecc48d1333..b0f60ce30d 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 c2ade89db8..cfef7f3718 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 752e303ba3..1d6b7f98e5 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 3a03c7fdc1..a18e2c681a 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 e91c95f52a..ab2ff6128d 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 ee295c5524..85816f9796 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 7212bcaf2c..19f8eec478 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 25fd08b4fd..1b58c074ae 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 64f3ff609a..4802d64abb 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 a201acc2b3..f9f72ca1ef 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 7298b6e798..c0e3217ac5 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 36fa855042..447104c963 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 9e58d56684..b5cf163da9 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 31b124665c..608adfe127 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 3a80fdc86b..31feafe504 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 f0ff1da070..59d6cdd481 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 1acaf7393d..35c2f5c57b 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 585ee7419f..0be97c28eb 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 899c5168ff..05b2c99bf6 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 79332ab6ac..2f0fac7900 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 e9ea7691ad..5847af4e96 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 5a5a964306..c7ae466dcb 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 1c9b19334a..ef20d7264c 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 1f8d1018c0..d5e70087f4 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 5cceb693a1..1244488006 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 f7e7d3c2f4..0fb7d4d15d 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 2cb42fd54a..b25376f9cc 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 8d44de8380..e7db829f33 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 810304219a..4f2d3154ff 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 dd1a9b01f9..b2e881c15f 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 8511d7ff85..5ffdf3a109 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 010854b60e..63dcddb955 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 afc2e08908..493385dae8 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 415987bc71..9a710c7a9c 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 a9e6d3d6fd..baeb6c3d24 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 651c51519b..2590a43408 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 438e31eb28..462ccb35d7 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 deacbf023b..f1ad0be2c9 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 0e12a494a1..77eff1562c 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 d8b037c26c..0fa4489138 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 6c1256a05f..0a2883aa84 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 91b0ab0450..0b8031c3f4 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 8ea47f7463..2aae0a53ae 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 e8c46c7d0a..902c542215 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 979b08de42..906fff4f67 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 8ba94b74eb..e9a5b0267c 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 21746e0dce..6f0e316b6a 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 92e135075e..9184d03a49 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 3147c3765b..78ead037d7 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 bad7fdb4a2..5bf1f3e589 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 780fa27827..b0e43b45d7 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 c4d0856a14..01c11b7b93 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 0407f016a3..1827d9fbba 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 23a80020d1..b8fb8d86d7 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 225d7fa065..7267918505 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 31e2a58c5d..29520d9116 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 af94121f56..3e7afa2432 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 1b58c074ae..3992508946 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 ab2ff6128d..d30e38bb84 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`)