43 Commits
Author SHA1 Message Date
Nicolas De Loof eb0fb65e0c docs(provider), e2e: image distribution documented and exercised
extension.md gains the Image distribution section: invocation contract
(--image/--digest/--created/--source/--policy), the get-image request
and its chunked image-stream answer (zero-chunk terminated, stdin
exclusivity and drain-before-next-answer contract), the two-branch
decision guidance (digest as identity, created as ordering fallback,
reproducible builds caveat), and the support-by-presence metadata
convention is now stated as the general rule. The example provider
implements pull end to end — request, stock chunked reader, hash
recorded at PROVIDER_PULL_MARKER — and the e2e scenario locks the
whole path: up builds the service image and streams it to the provider
under the local verdict, compose pull re-invokes it under the
freshness contract.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-10-02 17:35:20 +02:00
Nicolas De Loof 0a64ff09a8 fix: address copilot-pull-request-reviewer's 3 findings on the hook-runner plan
A GitHub Copilot PR review (docker/compose#14221#pullrequestreview-5300897216)
found three real issues in the reconciliation-plan-based hook runners, all
confirmed by direct reproduction against a live daemon before being fixed:

- **Hook runners excluded from discovery, stale runners persist.**
  collectObservedState's single getContainers call requires ConfigHashLabel
  (getDefaultFilters), which hook runners deliberately never carry -- so they
  never reached the classification loop that populates HookContainers, and
  planPreStartHookRunners never saw a stale runner to purge. Reproduced: a
  failed hook run followed by a second `up` failed with "container name ...
  already in use" instead of recovering. Fixed by listing hook containers
  separately, by project+hook label alone (new getHookContainers), and
  merging them in before classification. Locked in by a new e2e scenario,
  TestPreStartHookRetryAfterFailureSucceeds, that reproduces the exact
  failure-then-retry sequence.

- **Prepared hook runners misclassified as service replicas.** start's
  containers.filter(isService(name), isNotOneOff) has no reason to exclude a
  hook runner: it carries the same project/service labels as a real replica
  and no one-off label either. Added isNotHookContainer to the filter chain
  in startService, so a hook runner is never counted as (or started as) a
  surviving replica.

- **Per-replica hooks create runners before unsupported configuration is
  rejected.** per_replica is not yet supported (#14259): runPreStart
  validates every hook up front and rejects the whole service before
  executing any of them, but planPreStartHookRunners had no equivalent
  check, so create/up would plan (and leave behind) runners for a service
  whose hooks could never run. Mirrored the same up-front, all-or-nothing
  validation in planPreStartHookRunners.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-30 12:03:10 +02:00
Nicolas De Loof eaf172eab2 feat: reconciliation plan creates pre_start hook runners
pre_start hook containers become first-class resources of the
reconciliation plan. The reconciler plans one OpCreateHookContainer per
declared hook — gated on the same predicate the start phase uses to run
the hooks: a replica to start and no running replica SURVIVING the plan
(a recreated replica is not running at start time, so its runners are
planned too). Runner creation depends on every replica node (the
executor resolves the VolumesFrom target against a final live view),
on the infrastructure, and on the purge of previously observed runners:
names are deterministic (<project>-<service>-pre_start-<i>) so repeated
plans converge on the same container instead of accumulating anonymous
ones, and a new HookIndexLabel ties each runner to its hook.

runPreStart becomes pure execution: it looks up the created-state
runner for each hook by label and never creates one. A declared hook
without a prepared runner is an actionable error naming the
reconciliation command (docker compose up <service>) — the accepted
consequence is that stop-then-start of a hooked service errors, since
runners are consumed on success (locked by e2e). Success removal,
failure retention and cancellation cleanup are unchanged.

Runners stay invisible to the start/ps container listings — they carry
no ConfigHashLabel, which getDefaultFilters requires — and down keeps
removing them by hook label.

Rebased onto main, which diverged substantially since this branch was
opened (the unrelated container-spec-layering refactor, #14093,
restructured compose-go's ServiceConfig/PreStartHook types this PR also
touches). Reconciling pre_start.go/pre_start_test.go against that
refactor surfaced a real bug: createPreStartContainer builds the hook's
config via the shared getCreateConfigs, which unconditionally stamps a
ConfigHashLabel — silently contradicting this same commit's own
"invisible to ps/start" guarantee above, since that label is exactly
what getDefaultFilters requires. Fixed by deleting the label after
getCreateConfigs returns, with a test asserting its absence (an
independent local review, via the GitHub Copilot CLI, and a
fresh-context review agent both converged on catching this during
rebase verification). Also restored two security/label tests that only
existed on one side of the rebase (SecuritySensitiveFieldsHonored,
HookLabelsMerged), ported to call createPreStartContainer directly.

Also addresses glours' three review comments on this PR: hook-container
create operations for a service no longer chain serially (each depends
on the same shared containerNodes+infraDeps+purges set, letting the
executor create them in parallel), the corresponding plan-string test
assertion, and a clearer missing-runner error message hinting this
isn't necessarily a planning bug.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-30 12:03:10 +02:00
Nicolas De Loof 32bddfc4c6 fix: provider migration condemns the service's stale replicas
A service migrated from regular replicas to a provider left its old
containers standing: not converged (a provider service has no replicas
to converge), not orphaned (the service is still in the model). The
first up after the migration then failed creating the relay, because
the leftover replica still held the service's canonical container name
the relay takes over. And even without the name conflict (replicas
2..n of a scaled service), stale replicas kept the service's network
alias, competing with the relay on DNS and serving outdated traffic.

The reconciler now plans a stop+remove for every observed non-relay
container of a provider service, and the RunProvider node depends on
those removals: the canonical name is free by the time
ensureServiceRelay creates the relay. The relay itself (RelayLabel) is
the provider service's legitimate container and is left alone —
converged by ensureServiceRelay, not by the plan.

The e2e scenario migrates a deployed service to a provider and locks
the replacement (Recreated, relay reachable at the compose-native
address); the Scenario DSL gains FromFile — a local mirror of
FromRemote — to switch the model between steps.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-25 15:58:18 +02:00
Nicolas De Loof 207a3eb6f7 test(e2e): reproduce --project-directory ignored on an oci:// project
TestOciRemoteProjectDirectory publishes a project whose only service
mounts a relative volume, then runs `up` on the oci:// artifact with an
explicit --project-directory pointing elsewhere. The relative volume
must resolve against that directory.

It currently fails against the pinned compose-go: LoadConfigFiles
defaults the working dir to the downloaded artifact's own cache
directory whenever a remote resource loader is involved, silently
overriding the explicit --project-directory the same way it would a
mere default -- docker/compose#14224. On Docker Desktop this surfaces
as a hard failure (the cache directory isn't a shared mount); on Linux
it silently mounts the wrong directory, matching the original report.

New BindMountSource check (pkg/e2e/checks.go) pins a service's bind
mount source to an exact expected path, for tests that need to verify
which working directory a relative volume path resolved against.

Needs the compose-go fix (github.com/ndeloof/compose-go@a626c70,
branch 14224-working-dir) to pass -- not yet reflected in go.mod here.
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-23 10:56:23 +02:00
Nicolas De Loof 7e23119934 test(e2e): reproduce #14223 -- additional_contexts to a disabled service
A service reachable only as another service's build additional_contexts
must still build even when its own profile is inactive: it's referenced
for its image, not started as a workload -- the same tolerance the
depends_on consistency check already grants a disabled, non-required
dependency, and the same case addBuildDependencies already special-cases
on the docker/compose side.

compose-go's own consistency check doesn't grant that tolerance yet: it
rejects the reference outright, failing the whole project load before
addBuildDependencies (or anything else, up included) ever runs -- for
up, build and up --no-build alike, exactly as reported.

This is expected to fail until compose-go bumps to include
compose-spec/compose-go#931.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 18:01:16 +02:00
Nicolas De Loof 0d206e6704 fix(compose): move the job-target refusal into projectOrName itself
The previous fix only covered create/start. projectOrName backs every
other service-targeting command too (stop, kill, pause, unpause, logs,
rm, down, ps, events), so they still surfaced the same two problems: a
raw "no such service" error for a job name, and -- when
COMPOSE_PROJECT_NAME is set -- a silent no-op, since projectOrName's
label-driven fallback swallows the load error entirely.

Fold the check into projectOrName so every caller is fixed at once,
instead of duplicating the wiring per command. start.go's own
job-handling code, now redundant, is removed; jobTargetErr and
unselectedJobs (cmd/compose/run.go) are unchanged, just called from one
more place.

Adds unit coverage for projectOrName itself (including the
COMPOSE_PROJECT_NAME path and the typo-correlation edge case) and e2e
coverage for two more callers (stop, down) to confirm the fix lives in
the shared helper rather than being re-implemented per command.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De Loof 039413cc77 fix(start): still refuse a job when COMPOSE_PROJECT_NAME masks the error
projectOrName falls back to a label-driven, file-less project on any
load failure when COMPOSE_PROJECT_NAME is set -- swallowing the load
error entirely, including "no such service" for a target that's
actually a declared job. Since that branch returns err == nil,
jobTargetErr (gated on err != nil) and rejectScheduledJobs (gated on
project != nil) were both skipped: `docker compose start migrate`
fell through to a label-driven start that finds no container for a
job that was never run, and exited 0 having silently done nothing.

Refuse a named job explicitly on that fallback path too, reusing
unselectedJobs from the previous commit.

Also adds the test coverage this and the previous fix (job-target
error messages on create/start) were entirely missing: create.go and
start.go had no unit tests at all, and pkg/e2e/jobs_test.go only
exercised up/run.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De Loof 56bc854f37 fix(run): manual: false also blocks a job pulled in transitively
materializeJobClosure walked job-typed depends_on edges without
re-checking triggers.manual: false on anything but the top-level run
target, so `docker compose run A` where A depends_on job B (manual:
false) silently ran B anyway.

manual: false declares a job harmful to trigger outside its
schedule; depends_on doesn't change who caused the execution or when
-- pulling B in to satisfy A's dependency is still the run command
causing that out-of-schedule execution, one hop removed. Refuse it
the same way the top-level check already does, before anything in the
closure is created.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De LoofandClaude Fable 5 80cae9a7a5 pre_start: consume the load-time-resolved hook specification
compose-go resolves each hook against its service at load time: the
model itself carries the full container specification a hook runs with,
and the runtime consumes it as-is through the standard create path —
the runtime-side merge helper goes away.

Consuming the full spec means honoring ALL of it:
- service references it may carry — volumes_from entries, and
  service:-scoped network_mode/ipc/pid, inherited or declared — resolve
  to live container IDs exactly like the service create path does; the
  daemon knows nothing about service names and rejected them, failing
  up for any service combining volumes_from with a pre_start hook
  (locked by an e2e scenario).
- hook labels — declared or inherited — merge into the container's
  labels, the runtime identification set winning on conflicts.
- the <API 1.44 network-connect fallback joins the HOOK's networks,
  not the parent service's: a hook overriding networks was connected
  to the wrong ones.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De Loof 2804f9d4ae run: honor the explicit manual: false opt-out on jobs
Per the spec, any job can be triggered manually by an explicit run
command, its automated triggers notwithstanding — so run now accepts
scheduled jobs too. The exception is a job explicitly declared with
triggers.manual: false, which run rejects: meant for scheduled jobs
whose out-of-schedule execution would be harmful.

compose-go is bumped to the jobs-branch commit making Manual tri-state
(*bool) and allowing manual and schedule to be combined.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De LoofandClaude Fable 5 135fb0ed3a e2e: jobs scenarios — scheduled rejection, manual run, env_file, job chain
Scenario-DSL coverage for the jobs entry points:
- up refuses a project declaring an active scheduled job before creating
  any resource;
- run executes a manual job like a service, starting its depends_on
  services first, and refuses a schedule-only job;
- a job's own env_file feeds its environment through run — the
  materialization happens before environment resolution;
- a job depending on another job runs the dependency to completion
  first, through the exact machinery a service dependency uses.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De Loof ef277d266b pre_start: inherit the whole container specification generically
pre_start hooks carry the full container specification, but the
init-container runner only consumed a handful of attributes (image,
command, user, env, workdir) — everything else was silently dropped.

Instead of wiring attributes one by one, the hook's specification is
merged over the service's ContainerSpec through compose-go's own file
merge machinery (override.Merge on the canonical yaml tree): command
and entrypoint replace, environment merges per key with the hook
winning, extra_hosts and dns accumulate entries, ulimits merge — the
exact per-attribute rules of multi-file compose, maintained in one
place. Every ContainerSpec attribute inherits this way, current and
future, with zero attribute-specific code.

The merged spec then runs through the standard create path
(getCreateConfigs) as a service-shaped one-off, so resources,
capabilities, dns, sysctls, logging... materialize exactly as they
would for a service container. Hook containers keep their minimal
labels and carry no container-number, so tooling telling replicas
apart does not count them.

The only deliberate exception is volumes: mounts inherit at runtime
through volumes_from — the only mechanism that shares the service's
anonymous and image volumes — and the hook's own volume declarations,
materialized by the standard path, take precedence per target. This is
what lets an init container get read-write access to a volume the
service mounts read-only (fixes: see PR).

e2e scenarios lock extra_hosts inheritance and accumulation, volume
override and completion, and the unit tests pin the merge rules.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-21 16:35:20 +02:00
Nicolas De Loof aab5819147 feat(provider): publish-endpoint deploys a network relay for provider services
A provider's resource lives outside the compose network: consumers could
only reach it through injected variables carrying a host-published
address — nothing like the compose-native experience of addressing a
service by name at its well-known port.

A provider may now publish where each endpoint of its resource actually
listens:

    {"type": "publish-endpoint", "message": "80=localhost:49152"}

The endpoint is announced as seen from the provider's host: the relay —
the component that knows it runs inside a container — rewrites loopback
or unspecified upstream hosts to host.docker.internal (resolved through
its injected host-gateway extra_host); routable addresses pass through.

When at least one endpoint is published, compose deploys a relay
container in place of the service: a minimal TCP forwarder (new relay/
directory, published as docker/compose-relay, overridable with
COMPOSE_RELAY_IMAGE for internal registries) joining the networks of the
services that depend on the provider service, aliased with the service
name. Consumers then use http://<service>:<port> as if the service were
a regular container.

The relay is a first-class project container — canonical name, standard
compose labels including config-hash (label-driven commands run without
the compose file keep seeing the service: ps, logs, stop, down) — plus
the com.docker.compose.relay label declaring its role:

- the reconciler already leaves provider services' containers alone, and
  the relay's identity hash (image + routes) makes up idempotent: kept
  when routes are unchanged, recreated otherwise;
- process-level commands (exec, cp) refuse a relay — there is no service
  process in it to act on;
- the up monitor excludes relays from the containers whose termination
  ends an attached up: they are long-lived infrastructure and would
  otherwise keep 'up' waiting forever.

The example provider demonstrates the flow behind PROVIDER_DEMO_ENDPOINT
(a detached helper serving a fixed HTTP response), backed by an e2e
scenario asserting the compose-native address works and exec is refused.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-16 17:03:23 +02:00
Nicolas De Loof 3a5353d4ec e2e: cover git and OCI remote stacks
The remote loaders had almost no end-to-end coverage: one test consumed
an OCI artifact (config only, plus an up declined at the prompt) and
nothing exercised the git loader at all.

Git scenarios run against a throwaway repository served over the smart
HTTP protocol by an in-process server — git http-backend as a CGI per
httptest request: no daemon, no container, no fixed port. Smart HTTP is
a hard requirement: the loader ls-remotes the ref then shallow-fetches
the raw commit, which the dumb protocol cannot serve (no shallow
capability) and which needs uploadpack.allowAnySHA1InWant. Covered:
deploying from the default branch with repository-relative files,
selecting a #branch, selecting a #ref:subdir project.

OCI scenarios publish the fixture to a throwaway local registry (the
TestPublish pattern) then deploy from oci://: a full up consuming the
bundled env-file layer, and tag selection between two published
revisions.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-16 16:53:33 +02:00
Guillaume Lours 22d170f807 test(e2e): add attached up regression test for #13985
Unit tests alone didn't cover the exact failure: an attached up
hanging when the project is stopped/removed externally while a
service sits in restart backoff. The new test reproduces that
sequence end to end, so a future regression on this path breaks
loudly instead of slipping through as a passing unit suite. A
RestartCount guard narrows the race with the backoff expiring, so a
missed window fails loudly instead of passing silently through the
pre-existing die-based path.

Signed-off-by: Guillaume Lours <[email protected]>
2026-09-16 07:54:38 +02:00
Guillaume Lours 8018a96fde feat(compose): warn on unsupported compose-file attributes
Compose silently ignored several spec-valid attributes it doesn't
honor outside Swarm mode: deploy.update_config, credential_spec,
ports[].mode: host, and others had no visible effect on a plain
`docker compose up`.

Detection is wired into compose-go's loader as a fail-closed
allowlist: every attribute path the schema declares, minus the
handful this runtime deliberately doesn't implement, plus a few
value-conditional checks for attributes only unsupported for one
specific value. A newly-specified attribute gets flagged
automatically, with no corresponding code change needed here.

Depends on an unreleased compose-go change; go.mod carries a
temporary replace to a fork branch until it's released.

Signed-off-by: Guillaume Lours <[email protected]>
2026-09-09 16:32:04 +02:00
Nicolas De Loof a255af9368 feat(provider): get-service-config provider request
Providers could not see the definition of the service they manage:
options had to be duplicated between the compose file and the provider,
or the provider had to re-resolve the model on its own.

A provider may now emit {"type": "get-service-config"} on stdout;
compose answers on the provider's stdin with one JSON line holding the
resolved canonical configuration of the provider's own service,
straight from the in-memory model. The message can be repeated; each
occurrence is answered with one line. Detection is by construction: a
compose that predates the message aborts on it and never writes to
stdin, so the provider treats EOF as 'unsupported, upgrade compose'.

The example provider demonstrates the round trip, backed by an e2e
scenario; unit tests drive executePlugin against a helper-process
provider and cover the injection.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-08 14:53:14 +02:00
Guillaume Lours a896529193 fix(bridge): confirm before wiping non-empty convert output dir
`bridge convert -o <dir>` unconditionally ran `os.RemoveAll` on the
output directory and discarded any error, so a typo (`-o .`, `-o
$PWD`, `-o ~`) silently destroyed unrelated user data.

An empty or missing output directory is still created silently, but a
non-empty one now requires explicit confirmation (prompted
interactively, or via the new `--yes` flag for scripts/CI) before
being wiped.

Signed-off-by: Guillaume Lours <[email protected]>
2026-09-03 14:38:41 +02:00
Nicolas De Loof 06f4f9d622 fix: dependency wait timeout surfaced only by luck
waitDependency returned nil whenever its context ended, so an expired
wait deadline was reported only when the 500ms poll ticker happened to
win the select race and the condition check then failed on the expired
context. Whether `up --wait --wait-timeout` failed or silently
succeeded after the timeout was a coin toss, and the "timeout waiting
for dependencies" mapping in waitDependencies was unreachable on the
direct path.

Return the context error when the deadline expired; a plain
cancellation (Ctrl-C) is still not reported as a dependency failure.
The unit test pins both sides deterministically (a deadline shorter
than the first tick used to be swallowed with certainty); the e2e
scenario documents the user-visible contract: `up --wait
--wait-timeout` on a never-healthy service fails with "application not
healthy after Xs" while the container keeps running.

Part of #14074 (section C) / #14081 (lot 0).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:16:57 +02:00
Guillaume Lours 3a62ae63ce fix(ps): honor psFormat from Docker CLI config when no --format flag given
The --format flag defaulted to "table" instead of "", so the
opts.Format == "" guard in runPs never fired for a bare compose ps.
Setting the default to "" lets the existing config-fallback path work,
matching the behaviour of docker ps.

Fixes #13643
Supersedes #14032

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-20 14:05:34 +02:00
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
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 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