Commit Graph
1563 Commits
Author SHA1 Message Date
Guillaume Lours 8bd79da4bc e2e: fix test resource isolation and make standalone parallelism configurable
Several e2e tests used fixed host ports (8070, 8080, 8090, etc.), global
buildx builders registered with --use, and a hardcoded IPC container name.
Under parallel execution these shared daemon-level resources caused port
conflicts, buildkitd container-name collisions, and intermittent failures.
Separately, the standalone make target hardcoded -parallel=1, forcing the
entire standalone suite to run serially even though the same tests run in
parallel in plugin mode.

Changes:
- Convert fixed host ports in four fixtures (network-test, sentences,
  build-test, volume-test) to ephemeral bindings; add ServicePublishedPort
  helper to framework.go to resolve the actual mapped port at runtime.
- Give each buildx-builder test a unique daemon-scoped name via BuilderName
  helper, preventing container-name collisions between parallel goroutines.
  Applies to TestBuildPlatformsWithCorrectBuildxConfig, TestBuildPrivileged,
  TestBuildBuilder, TestBuildEntitlements, TestBuildTLS.
- Rename the fixed ipc_mode_container to a project-scoped name
  (ipc_e2e-src) and update the ipc-test fixture accordingly.
- Replace the hardcoded -parallel=1 in make e2e-compose-standalone with
  E2E_PARALLEL_PLUGIN?=4 and E2E_STANDALONE_PARALLEL?=4, aligning both
  modes with the effective GOMAXPROCS on 4-core GitHub Actions runners.
  Both variables are overridable (E2E_STANDALONE_PARALLEL=1 restores
  serial local debugging).
- Raise the poll delay in watch_test.go long-running polls (90s/120s
  timeout) from the default 100ms to 1s; bump cascade_test.go and
  publish_test.go from 100ms to 500ms. Reduces CPU pressure without
  affecting effective test duration.

The logging-driver fixture retains its fixed port 24224: the Docker fluentd
log driver encodes fluentd-address at service-start time, making ephemeral
resolution non-trivial. TestLoggingDriver is serial so there is no
collision risk in practice.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-19 17:41:02 +02:00
Deniss Solnce f8ed846e4d fix(hooks): include hook output in the error when a hook fails
A failing service hook returns only its exit code:

    dependency failed to start: db hook exited with status 1

The output that explains why is not there, and for a hook running without a
listener it does not exist anywhere: runHook created the exec with
AttachStdout/AttachStderr false, so the daemon discarded both streams and
runWaitExec only polled for the exit code. There was nothing left to look at
afterwards.

Now stdout and stderr are always attached, and the last lines of them are kept
in a bounded buffer. On non-zero exit that tail goes into the error:

    db hook exited with status 1: SQLSTATE[42S02]: Base table or view not found

A successful hook is unchanged - output goes to the listener as before, nothing
is added anywhere. With a listener the lines still stream out, the buffer just
tees them.

The buffer keeps the last 10 lines and 2 KiB, drops the oldest content and never
short-writes, so a chatty hook cannot block on it and cannot grow it.

runWaitExec is removed: it existed only for the unattached branch, and that
branch is what threw the output away.

Signed-off-by: Deniss Solnce <[email protected]>
2026-08-19 15:08:25 +02:00
Nicolas De Loof 8b654eba78 docs: document engine container event sequences in up monitor
Map, per container lifecycle scenario, the exact event sequences the
engine emits and the monitor may receive: natural exit, policy-driven
restart (die/start only, no restart action, silence during backoff),
external stop of a running vs restarting container, ContainerRestart,
docker kill, OOM kill, rm -f and rm.

Mapped from moby's daemon sources (LogContainerEvent call sites and
handleContainerExit/containerStop/containerRestart/kill paths) and
verified against a live engine v29.6.1 with 'docker events'. Notably:
the relative order of stop and die on a container stop is not
guaranteed, and no event at all is emitted while a container sits in
restart backoff.

Related to #13985 / #13990.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-19 13:46:30 +02:00
Endika Iglesias f519180b95 fix(watch): sync into a symlinked directory instead of failing
Signed-off-by: Endika Iglesias <[email protected]>
2026-08-18 17:25:13 +02:00
Nicolas De Loof 115898dc13 test(e2e): address review — project-name collisions, file modes, regex compilation
- projectNameFor maps the subtest separator to a double dash so
  TestFoo/bar and TestFooBar cannot resolve to the same compose project
  and interfere when running in parallel
- copyDir preserves the source file mode instead of forcing 0644, so an
  executable testdata script stays executable in the scenario's project
  directory
- OutputMatches and OutputMatchesCount compile their pattern once at
  construction: a malformed pattern panics where the check is declared,
  and Eventually loops no longer recompile it on every tick

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof aef01ae584 test(e2e): batch snapshot inspect with per-container fallback, explicit suite timeout
The per-container inspect of the previous fix doubled the suite's
wall-clock on the containerd CI job — one docker CLI process per
container per snapshot — pushing it past go test's default 10m and
killing the run mid-flight. Snapshots go back to one batch inspect,
falling back to individual inspects only when the batch fails because
a helper container vanished. The e2e make targets also pass an
explicit -timeout 20m so a slow job fails on its own results, not on
the default timeout.

TestUpStopWithLogsMixed becomes deterministic: with a bounded ping the
service could exit on its own before the abort reached it and the
pre_stop hook never ran; it now pings forever so the abort always
interrupts it.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 880c5a2f1d test(e2e): snapshot must survive vanishing helper containers
The plugin/stable/containerd CI job caught a race the DSL had: hook
helper containers carry the project and service labels but no replica
number, and can be destroyed between the snapshot's ps listing and its
batch inspect — which then failed and blanked the entire snapshot,
making ServiceState report 'service has no container' while the
container was right there. Containers are now inspected one by one,
tolerating disappearances, and helper containers (neither numbered as
a replica nor one-off) are excluded from snapshots altogether so they
can't transiently pollute service checks either.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 1fa9ae160a test(e2e): image-identity, images and model scenarios
- TestUpIdempotentContainerdStore joins the scenario style of its
  corner-case siblings: Serial + ContainerdImageStore requirement, the
  regression story in a comment, NotRecreated as the invariant. The
  requireContainerdStore helper and the last identity fixture go away.
- TestImagesAfterImageRemoved scopes its throwaway tag to the project
  and observes container state before pulling the image record away.
- TestComposeModel inlines its model; verified in CI where the
  docker-model plugin is installed (the local runner cannot pull the
  model, as with the legacy test).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 447157274e test(e2e): build scenarios — the 'Built' family inlines its models
TestBuildDependsOn, Subset, DependentImage, SubDependencies,
LongOutputLine, DependentImageWithProfile, Escaped, Tags and Secrets
become scenarios: models inline (the long-line Dockerfile generated
with strings.Repeat instead of 34 copy-pasted lines), image names
project-scoped with Defer cleanup, and the secrets build asserts
ImageExists on top of the Dockerfile's own diff checks.

Kept legacy: TestLocalComposeBuild (DOCKER_BUILDKIT=0/1 matrix over a
shared fixture with fixed names and host port), the buildx-builder
family (SSH, platforms, privileged, entitlements, builder, TLS) which
provisions builders and dind daemons, and TestBuildImageDependencies
(classic/bake matrix). Their fixtures under build-test/ stay; the
single-purpose subdirectories are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a7293130c6 test(e2e): compose command scenarios — rm, compatibility, config, orphan scoping
- TestRm, TestRemoveOrphaned and TestUnnecessaryResources observe which
  containers exist instead of grepping docker ps.
- TestCompatibility drops the heavyweight sentences fixture: underscore
  naming shows on any model.
- TestConfig/TestConfigInterpolate build their expected rendering from
  the scenario's own project name and directory; OutputMatches joins
  the vocabulary for TestInitContainer's ordering assertion.
- TestStopWithDependenciesAttached inlines its model.
- Kept legacy: TestLocalComposeUp (fixed port, HTTP, shared sentences
  fixture), TestDownComposefileInParentFolder (project name inferred
  from a fixture-relative directory), TestAttachRestart (long-running
  attached up), the COMPOSE_FILE-from-.env family and TestNestedDotEnv
  (they exercise invocation without -f, which ComposeCmd always sets).
- Fixtures init-container/, external/ and dependencies/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 37f57744de test(e2e): exec and provider scenarios
- TestExec locks the container-selection order (service container over
  one-off) with one-off state observed at each step.
- providers: a shared providerScenario helper wires the
  example-provider binary into PATH; the env assertions anchor on the
  'test-1  | ' log prefix, replacing the hand-rolled env parser, and
  the stop-hook sentinel file is observed with FileExists.
- Fixtures exec/ and providers/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof f4f6ac5324 test(e2e): volume and build-once scenarios
- volumes: external-volume switching, volume recreation on definition
  change and image volumes become scenarios; the two-compose-file
  fixtures collapse into one model with an interpolated volume name or
  label, so the changed definition is visible in the test. The rebuild
  scenario locks the Recreated expectation and scopes the source image
  to the project. TestLocalComposeVolume (fixed host port, HTTP) and
  TestProjectVolumeBind (host file written mid-test) stay legacy.
- build-once: the three run --build regression cases each get a
  scenario; OutputMatchesCount joins the vocabulary to count the build
  reports, replacing the hand-rolled regex counter, and image names are
  project-scoped instead of random suffixes.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof ee7f323fed test(e2e): run and up scenarios
- compose run splits into seven scenarios (one-off basics, ports, deps,
  optional deps, quiet pull, --pull always, chained build contexts).
  The orphan-warning cases fold into the basic scenario: an exited
  one-off IS the orphan the next run warns about, no second compose
  file needed. Piped-stdin and stop-signal tests stay legacy.
- up/compose_up: unhealthy service, dependency exit (both now also
  locking that dependents stay in created state), build dependencies
  (project-scoped image name instead of the fixture's global one),
  optional dependency, --all-resources, profile targeting (dropping the
  fixture's global container_names), image-by-ID, exit-code-from (42
  and 143), port ranges, stdout/stderr separation (StderrContains joins
  the vocabulary) and logging-driver reconfiguration (now locking the
  Recreated expectation). The Ctrl-C dependency test stays legacy.
- Fixtures start-fail/, resources/, stop/, port-range/, stdout-stderr/,
  logging-driver/, profiles/ and most of dependencies/ and run-test/
  are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 9409c461f7 test(e2e): publish-check scenarios — WithStdin answers the prompts
The dry-run publish checks split into nine scenarios, one per model
and refusal reason; the prompt text, refusal exit code (130) and
publication report are the observables. WithStdin joins the Action
modifiers to feed prompt answers. TestPublish stays legacy: it drives
a real registry container and loads oci:// projects with multiple -f
files. Only the oci/ fixture subtree it needs survives.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof d38fa3431a test(e2e): cp, environment and config scenarios
- cp: the twelve copy directions become one scenario; Dir() joins the
  Scenario API for actions that exchange files with the host, and
  FileContains observes what landed there. The files copied in and out
  live in the txtar archive instead of writing into the fixture tree.
- environment: TestEnvPriority splits into three scenarios, one per
  compose model (environment section, bare, env_file section), each
  declaring its .env variants inline; StdoutContains joins the
  vocabulary so container output cannot collide with progress noise
  (the unused framework predicate of the same name is removed). The
  fixed 'env-compose-priority' image name becomes project-scoped.
- config: rendering assertions move to scenario steps; the config
  output is the observable here by definition.
  TestConfigHashMatchesContainerLabel stays legacy: it cross-references
  hashes captured at runtime, which checks cannot hold.
- Fixtures cp-test/, environment/ and config/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 816709d877 test(e2e): exec and lifecycle-hook scenarios
- exec: exit-code propagation, -e env forwarding and one-off targeting
  become two scenarios with an inline model.
- hooks: the thirteen post_start/pre_stop/pre_start tests migrate. The
  volume probe is a named action (probeVolume); wc -l counts are locked
  by matching the exact '1 /mnt/tokens.txt' line instead of parsing.
  The spec-change scenario replaces its two fixture files with one
  model interpolating ${HOOK_VERSION}, so the changed spec — and the
  Recreated expectation it implies — is visible in the test source.
  pre_stop scenarios drop the fixture's fixed 'sample-data' volume
  name, which parallel projects used to share.
  Fixtures hooks/ and pre_start/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 54caa0775c test(e2e): pull and network scenarios
- pull: the CLI's Pulled/Skipped report is the observable, so the five
  scenarios (always-pull, present-locally, build-only, failure,
  --ignore-pull-failures) legitimately match output; models are inline
  and the whole compose-pull fixture tree — including the orphaned
  duplicate-images directory no test referenced — is removed.
- networks: aliases, links, ipam, network_mode: service, subnet change,
  mac_address and interface_name become scenarios. The subnet-change
  scenario locks what actually happens: the network is replaced but the
  container is reconnected, not recreated (NotRecreated), which the
  legacy test never distinguished. TestNetworks (fixed host ports,
  HTTP fixture services) and TestNetworkRecreate (OR-shaped output
  assertion) stay legacy.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof cd1c0ca6ca test(e2e): configs, commit, export and logs scenarios
ImageExists and FileExists join the vocabulary, giving commit and
export a real observable: the legacy tests only asserted the commands
exited 0, the scenarios verify the committed image and the exported
archive actually exist. Both also drop their fixed image tags and
cwd-relative tar outputs for project-scoped tags and temp files, so
parallel runs cannot collide and nothing is left behind in the repo.

TestLocalComposeLogs waits for the echo service to exit before reading
logs (Eventually), closing the race the legacy test tolerated. The
follow and large-logs tests stay legacy: both drive a long-running
command in the background. ps_test.go stays legacy as well — it
verifies ps table/JSON formatting, which is inherently output-shaped.

Fixtures configs/, commit/ and export/ are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 3923931b8c test(e2e): scale, secrets and env_file scenarios
ServiceScale and ReplicaNumbers join the vocabulary: replica counts and
numbering read from container labels replace counting 'Started' lines
in the CLI output.

- TestScaleDownNoRecreate's final assertion now actually runs: the
  legacy test built the scale-down command without executing it, so
  'scale down removes obsolete replicas' was never exercised; the
  scenario locks the real behavior (stale replicas dropped, up-to-date
  ones kept, whatever their numbers).
- secrets scenarios inline the include-based project (compose.yaml,
  child project and env_file as one txtar archive).
- TestRawEnvFile and TestRunEnvFile inline their env files;
  TestUnusedMissingEnvFile stays legacy for its completion assertions,
  which drive the plugin/standalone binaries directly.
- Fixtures scale/, env-secret/ and dotenv/raw are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof fe17e31be0 test(e2e): lifecycle scenarios — cascade, profiles, healthcheck, recreate
Vocabulary grows with Recreated (the mirror of NotRecreated),
ServiceHealthy (healthcheck status observed from inspect) and a Within
action modifier bounding blocking commands like up --wait, replacing
the hand-rolled goroutine+timer of the legacy TestStartInterval.

- cascade: abort-on-container-exit/failure scenarios inline the model;
  exit-code propagation (111) is locked with ExitCode. The one-off
  variant stays legacy — it drives a long-running up in the background.
- profiles: the four scenarios observe which services materialize
  instead of grepping ps output; the dotenv variant declares its .env
  inline, making the activation mechanism visible.
- TestRecreateWithNoDeps now asserts both sides: the service is
  replaced (Recreated) and the healthy dependency is left alone.
- Fixtures start_interval/, profiles/compose.yaml+test-profile.env and
  recreate-no-deps.yaml are removed; cascade/ stays for the legacy
  one-off test.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 6bc1b36516 test(e2e): expose, container_name, ipc and wait scenarios
- TestExposeRange keeps its regression link (#13378), the invariant
  being simply that up accepts a port range in expose.
- TestUpContainerNameConflict interpolates a project-scoped
  container_name instead of the fixture's global 'test', so parallel
  runs cannot collide on it.
- TestIPC demonstrates the CLI() escape hatch: the external container
  the 'container:' mode points at is created eagerly before the steps,
  so its id is available to the declarative checks. (Runs only in CI:
  ipc container: mode is rejected by Docker Desktop and dind daemons —
  'restricted host mount' — for the legacy test as well.)
- wait scenarios observe which services exited instead of trusting the
  command's silence; TestWaitOnInfinity stays legacy, the DSL has no
  notion of a still-running command. The wait fixture stays for it;
  ipc-test and container_name fixtures are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a33cc37ad9 test(e2e): TestRemoveOrphans states its real intent — COMPOSE_REMOVE_ORPHANS from .env
The legacy test read as 'up removes leftover one-offs', but plain up
only warns about them: the removal it asserted came from the fixture's
.env declaring COMPOSE_REMOVE_ORPHANS=true (added by 'restore
setEnvWithDotEnv', d3d378b92) — an invariant the fixture kept invisible.
The scenario declares both files inline as a txtar archive, so the
mechanism under test is in plain sight, and observes the one-off's
removal from container state rather than ps output.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a725a8ae04 test(e2e): one-off vocabulary — service checks skip run containers
Snapshots now record whether a container is a one-off (run) container,
plus its start time. Service-facing checks (ServiceState, NotRecreated,
label checks) observe only long-lived containers; three checks join the
vocabulary for the one-off side: OneOffState, OneOffsUntouched (same
containers, same state, same start time — catches an unwanted restart,
which message matching never could) and OneOffsRemoved (errors if there
was nothing to remove, rather than pass vacuously).

TestStartStopWithOneOffs becomes the demonstrator: stop/start/restart
act on services and leave the one-off untouched, down --remove-orphans
takes it away.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 695a2a759a test(e2e): start/stop scenarios observe container state, not CLI chatter
Migrate the state-friendly start/stop tests to the Scenario DSL:

- TestStartStop: stop halts containers in place, start brings the same
  ones back (NotRecreated); the ls checks match the project name exactly
  in the JSON output, immune to the prefix collisions the other
  start-stop project names would cause under parallel runs.
- TestStartStopWithDependencies: dependency start/stop propagation read
  from container state instead of 'Container X Started' messages.
- TestUpNoDeps replaces the 'Up no-deps links' subtest as its own
  scenario: up --no-deps must not create the linked dependency.
- TestStartSingleServiceAndDependency: create/start walk the dependency
  chain and nothing else — now asserting the unrelated services have no
  container at all, stronger than the legacy message matching.

The one-off and multi-file tests stay legacy: the vocabulary has no
one-off awareness yet and a scenario holds a single compose file.
The links and start-stop-deps fixtures become unused and are removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 5baeadf929 test(e2e): no-deps scenarios — the donor container's absence is the observable
Migrate the volumes_from and network_mode:service donor tests to the
Scenario DSL, inlining the compose model and dropping the no-deps
fixtures. Each scenario removes the donor container behind compose's
back and expects up --no-deps to be rejected with the 'container
missing' error — the CLI's reported decision being the observable.

ServiceNotCreated joins the check vocabulary: it asserts a service has
no container at all, here proving the donor really is gone before the
failing step.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 12288a7fff test(e2e): pause scenarios — paused is a container state, ExitCode locks error paths
Migrate the four pause tests to the Scenario DSL: the invariant is the
container state (paused/running), not an HTTP timeout, so the scenarios
observe state and drop the port-mapping helpers and the pause fixture.

ExitCode joins the check vocabulary for error-path steps, paired with
MayFail. TestPauseServiceDoesNotExist now locks the current behavior —
pause of an unknown service is rejected with 'no such service' — which
resolves the TODO the legacy test carried: it asserted exit 0 while
wishing for an error, and the CLI has since started validating selected
services.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof b2e3deb18e test(e2e): testdata/<TestName>/ anchors each scenario's project files
Each scenario resolves its project files from testdata/<TestName>/ by
convention: standalone compose files in their native format, directly
runnable with docker compose -f, copied to a temporary directory so the
committed files are never mutated. Subtests map to nested directories.

Ownership is strictly one test per directory: a check fails the suite on
any testdata directory no test owns, which is what keeps this from
degrading into a catch-all fixtures directory — no sharing, no
leftovers after a rename.

The inline Compose() declaration is removed; the demonstrator scenarios
move their models to testdata, including the multi-file build context
of TestUpBuildUnchangedContext (compose.yaml + Dockerfile + marker).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De LoofandClaude Fable 5 fa9b31c2fa test(e2e): checks.go as the reviewed vocabulary, SCENARIO.md codifies the contract
Checks are the shared vocabulary between scenarios: move them to a
dedicated checks.go so the whole vocabulary is read — and reviewed — as
one file, independently of the tests using it.

SCENARIO.md codifies the rules that so far only lived in the PR
description: one intent = one invariant, step names as behavior
sentences, inline compose model, state-based checks first with
OutputContains as an explicit last resort, new checks generic and named
after their observable — plus how to exploit the failure artifacts and
E2E_KEEP_FAILED. AGENTS.md points new e2e work at it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De LoofandClaude Fable 5 9ff4693312 test(e2e): dump failure artifacts, E2E_KEEP_FAILED keeps the project alive
On failure, a scenario now writes the untruncated diagnostic material to a
stable per-project directory (compose.yaml, each step's full command and
output, project containers, engine events, full container logs, per-step
state snapshots), announced at the top of the failure report. The inline
report stays truncated for readability; the artifacts let a failure be
diagnosed without re-running the scenario.

E2E_KEEP_FAILED=1 skips the teardown of a failed scenario so the live
project state remains inspectable.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof a443a6589d test(e2e): Eventually check, TestRestart as polling demonstrator
Add Eventually(check, timeout) to the scenario layer: it retries a
state-based check, re-observing the project containers between
attempts. ServiceState(service, state) joins the check library, backed
by a State field in the per-step snapshots.

TestRestart is converted as the demonstrator: it replaces a
WaitForCmdResult polling `ps --format json` for a JSON fragment and a
bare time.Sleep with two declarative expectations, and now also locks
the real invariant — restart must reuse the same container — via
NotRecreated, which the lock-file trick of the original fixture was
proving only indirectly.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De Loof 0632f222f6 test(e2e): declarative Scenario layer, image-identity tests as demonstrator
Introduce a small declarative layer over the e2e framework so a test
reads as its intent: an inline compose.yaml, a sequence of steps
(command + expected observables), and nothing else. The framework owns
project naming (derived from the test name), cleanup, and failure
reporting: on the first unmet expectation the test fails with a step
transcript, the failing command output, the project's container state,
the engine events since the scenario started and the container logs.

Checks observe real state (container IDs across steps, labels, image
manifest platform) instead of grepping the progress writer output,
which stays available as an explicit last-resort check.

The e2e tests introduced by #14041 are converted as a demonstrator;
their dedicated fixture files are inlined in the scenarios and removed.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 16:49:56 +02:00
Nicolas De LoofandClaude Fable 5 02bc297519 refactor: last gocognit candidates — bridge resources and TestViz
LoadAdditionalResources (cognitive complexity 31) moves its
per-service body to loadServiceImageResources, with the local-vs-pull
inspection decision in inspectServiceImage. TestViz (35) factors the
shared graph assertions into assertVizGraphNodes and
assertVizDependencyEdges (t.Helper), merging the allowed/forbidden
edge bookkeeping into one equivalent loop.

This removes the last two of the 19 //nolint:gocognit suppressions
introduced by the gocyclo->gocognit migration: the FIXME debt is fully
paid, the linter now runs suppression-free.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 c640808709 refactor: extract endpoint and volume translation decisions in create
createEndpointSettings (cognitive complexity 31) moves address parsing
to parseEndpointIPAM and the interface_name/driver_opts merge to
endpointDriverOpts. buildContainerVolumes (34) gets one decision
helper per mount type: bindStringForMount, volumeBindString,
checkImageMountSupported. A dead branch in the bind case (source was
reassigned to the same value) is simplified away — the surrounding
behavior is unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 10c058a57b refactor: split publish push path and sensitive-data scanning
publish (cognitive complexity 34) moves its non-dry-run push path to
pushComposeArtifact, with the application image index construction in
pushApplicationIndex. checkForSensitiveData (34) splits per source
kind: scanEnvFiles (per-service env files with the required/missing
rules) and scanFiles (file-backed configs and secrets, deduplicating
two identical loops).

No behavior change: same events, same errors, same findings.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 1d1b94f0c0 refactor: split doBuildImage context preparation stages
doBuildImage (cognitive complexity 43) mixed builder-capability
guards, context-type resolution (with in-scope defers), tar archiving
and credential conversion. Each becomes a function; the context
resolution returns a classicBuildContext carrying a cleanup so the
defers run at the same point they used to.

No behavior change: same errors, same context handling.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 33c6435825 refactor: flatten Ps, restart and Logs orchestration
Three command paths with the same shape — a setup phase then per-
container goroutines with inlined bodies:

- Ps (44): the goroutine body becomes containerSummary, with pure
  helpers for publishers, health/exit-code, mounts and networks.
- restart (42): project resolution moves to prepareRestartProject,
  the per-container body to restartContainer. Also fixes a latent
  data race: hooks and restart assigned their error to the function's
  outer err from concurrent goroutines; now local.
- Logs (31): container selection moves to selectLogsContainers, the
  streaming body to logContainer, and the follow-mode listener to
  followStartedContainersLogs.

No behavior change: same events, same warnings, same results.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 d6e26f427f refactor: split ociRemoteLoader.Load pull and index resolution
Load (cognitive complexity 48) mixed the enable/offline guards with
the artifact pull, the cache check and the image-index indirection.
The pull path moves to pullComposeArtifact and the index-to-manifest
resolution to resolveComposeManifest. Load reads as: guards, cache
lookup, pull on miss.

No behavior change, including the last-matching-manifest-wins loop
and the silent cache hit on a non-NotExist stat error.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 68ab763674 refactor: flatten watch trigger preparation and initial sync walk
watch (cognitive complexity 52) inlined three passes over each
service's triggers; they become prepareRebuildTriggers (validation +
always-build marking), watchTriggerPaths (paths to monitor, bind-mount
skips, initial sync) and initialSyncRequested (the DEPRECATED
x-initialSync fallback). initialSyncFiles (34) delegates its WalkDir
closure to initialSyncDirectory, keeping the Info() call order so
error behavior on unreadable entries is unchanged.

No behavior change: same validations, same warnings, same collected
paths and sync mappings.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 f46228a609 refactor: materialize compose pull scheduling as an imagePuller
pull (cognitive complexity 56) interleaved two scheduling loops
(service images, pre_start hook images) sharing dedup state, failure
slots and the must-build fallback list. That state becomes an explicit
imagePuller; the loops become pullServiceImages and pullHookImages,
the per-service goroutine body becomes runServicePull, and the four
copies of the Skipped event literal collapse into eventSkippedPull.

Also fixes a latent data race: mustBuild was appended from concurrent
pull goroutines without synchronization; it is now guarded by a mutex.

No behavior change otherwise: same skip events, same fail-fast rules,
same error report.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 0d81e99391 refactor: materialize interactive up as an upSession
Up (cognitive complexity 79) interleaved service creation with an
implicit session object: an errgroup, an error collector, exit status
and termination flag, all shared between six closures. That session is
now an explicit upSession type; each concern becomes a method:
runEventLoop (signals/keyboard/cancellation), stopApplication and
killApplication (the first/second-interrupt actions), stopOnFirstExit
(--abort-on-container-exit cascade), captureExitCodeFrom
(--exit-code-from), followStartedContainers + streamContainerLogs
(attach to (re)started containers). Keyboard menu setup moves to
setupNavigationMenu, closing the keyboard on the desktop-detection
error path exactly where Up's deferred Close used to.

Also fixes a latent data race: the graceful-stop and cascade-stop
goroutines assigned their error to Up's outer err variable (racing
with the main goroutine) before handing it to the collector; the
assignment was redundant and is now local.

No behavior change otherwise: same listeners in the same order, same
cancellation points, same error report.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 ef54fcaa41 refactor: one handler per container event in monitor.Start, add tests
monitor.Start (cognitive complexity 77) inlined the handling of all
four container actions in the event loop. Each action moves to an
onContainerX handler, with the recurring bits named: watched() (is
this service's container ours to track), notify() (broadcast to
listeners), initialContainers() (seed the tracking set). The loop
drops to complexity 19 and reads as: seed, subscribe, dispatch until
no containers remain. The tracking sets stay explicit parameters so
data flow remains visible.

The monitor had no unit test despite driving up/logs termination; the
refactor is locked by a new suite covering the full lifecycle
(created/recreated/started/restarted/exited, default-name trimming,
exit codes), the restart detection through both engine states
(Restarting, and Running per moby/moby#45538), the already-removed
container on die, service filtering, the no-container fast path,
events stream errors, context cancellation, and exit-code parse
errors.

No behavior change: same events, same ordering, same termination
conditions.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 bae663129e refactor: one function per depends_on condition in waitDependencies
waitDependencies (cognitive complexity 76) inlined the polling loop
and the per-condition logic for all three depends_on conditions, each
with its own optional-dependency degradation. The polling loop moves
to waitDependency, and each condition check becomes a function
reporting (done, err) — (false, nil) means keep polling. The driver
drops to complexity 11 and its FIXME suppression is gone.

No behavior change: same events, same log messages, same errors.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 13:37:48 +02:00
Nicolas De LoofandClaude Fable 5 bd7f14c186 fix: bake target names collision for services differing by '.' vs '_'
The dedup loop in bakeTargetNames checked whether the service already
had a name — never true on first pass — instead of whether the name
was already taken, so services like 'a.b' and 'a_b' silently mapped to
the same bake target and one overwrote the other in the bake file.
Allocate names in sorted service order (deterministic) and append '_'
until unique.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 11:41:00 +02:00
Guillaume Lours 81bb4afed5 fix(events): validate service names before subscribing to events
docker compose events <name> was passing service arguments straight to
the event filter without checking they exist in the project, so a typo
would produce an event stream matching nothing and block forever with
no output and no error.

Switch to projectOrName (which validates args via ToProject) before
calling backend.Events, consistent with logs, kill, and restart.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-18 10:48:59 +02:00
Guillaume Lours 8d763d9bff test(e2e): prepend provider bin dir to PATH in provider tests
When a locally-installed binary with the same name (e.g.
example-provider) sits in an earlier PATH directory than
bin/build/, exec.LookPath resolves the wrong binary.  The
old tests appended the build directory to the end of PATH,
so any pre-existing system binary shadowed the test one.
Prepending instead ensures the freshly built binary always
wins, regardless of what is installed on the developer's
machine.  CI is unaffected since no such system binary
exists there.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-18 10:47:48 +02:00
Nicolas De LoofandClaude Fable 5 6d2e1d99dd build: split doBuildBake at its responsibility boundaries
doBuildBake mixed five concerns in a 300-line body (cognitive
complexity 88): progress display setup, translation of the project
into a bake file definition, temp metadata file allocation, bake
command construction, stderr rawjson streaming, and result
collection. Each now lives in its own function; the driver reads as
the sequence of those stages (cognitive complexity 17), and the
FIXME suppression is gone.

The bake variable was named to avoid shadowing the docker/cli
'build' package. No behavior change: prepareBakeBuild emits the same
config, and the stderr loop keeps the decoder-per-line semantics.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 10:42:37 +02:00
Nicolas De LoofandClaude Fable 5 2340121f0e lint: replace gocyclo with gocognit
Cognitive complexity (gocognit, threshold 30 — the linter default) fits
this codebase better than raw cyclomatic complexity: it barely charges
guard clauses and early returns, and penalizes nesting instead. As a
result 8 of the 21 //nolint:gocyclo suppressions become unnecessary,
while deeply nested functions gocyclo never flagged are now caught.

The 19 functions still above the threshold keep a suppression, each
marked FIXME to complete the migration by restructuring them.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-18 10:42:37 +02:00
Nick Sieger 870908cc8f feat(ps): add ENGINE column gated on label presence
Show an ENGINE column in `compose ps` default table output when the
com.docker.compose.engine label is present. Expose {{.Engine}} for custom
--format templates.

Rename api.EngineLabel to api.ContainerEngineLabel to avoid the
name collision with desktop.EngineLabel (com.docker.desktop.address).

Signed-off-by: Nick Sieger <[email protected]>
2026-08-17 10:34:46 +02:00
Nicolas De Loof 1e80a0906f chore: inline needless single-use helpers
Inline small functions that were extracted from their single call site
without a real boundary to justify it — no reuse, no dedicated test, no
responsibility of their own — so each caller now tells its whole story
top-down:

- getExecTarget, attachContainer, logContainer: one-line trampolines to
  getSpecifiedContainer / doAttachContainer / doLogContainer, which
  other call sites already use directly
- removeImage: single-statement wrapper, unlike its removeVolume
  sibling which has actual logic
- checkSelectedServices: named like a validation, actually a filter;
  the subtle rule (an unknown service is only an error with an explicit
  compose file) now reads where options.Services is rewritten
- prepareLabels: mutated the map it received while looking pure at the
  call site; the label writes are now visible in getCreateConfigs
- setDefaultTarget: mutation-by-pointer of the loop copy, now visible
  in the loop of injectFileReferences
- buildVolumeOptions/buildTmpfsOptions/buildImageOptions: nil-guard +
  field copies; the buildMountOptions switch now shows side by side
  what each mount type propagates (buildBindOption keeps real logic and
  keeps buildMountOptions under the gocyclo limit)
- displayDryRunBuildEvent: was longer than its only caller
- hasMore: read like a predicate, was a one-line spinner restart
- escapeDollarSign: wrapped a single bytes.ReplaceAll
- extractEnvCLIDefined: replaced by the canonical compose-go helper
  types.NewMappingWithEquals().ToMapping(), as run.go already does
- isPullPolicyValid: rebuilt the valid-values slice on every call; now
  a package-level list checked at the call site
- viewFromStackList: projection now sits next to the render closure
  that consumes exactly its three fields

No behavior change; single gocyclo threshold untouched.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-14 22:49:53 +02:00
Nicolas De Loof df094b036c fix(images): tolerate containers whose image record is gone
A running container may reference an image record that no longer
exists: under the containerd image store, `up --build` with identical
content moves the tag to a new index digest (provenance attestation
churn) and the daemon drops the old index the container was created
from — without compose recreating the container, by design (#13636).
`docker rmi -f` of a running container's image produces the same state.

`compose images` used to fail the whole listing on the resulting
NotFound. Degrade to what the container itself knows (image ID, and
repository/tag when the reference is not a raw ID) instead of failing.

Fixes #14014

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-14 15:20:37 +02:00