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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
`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]>
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]>
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]>
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]>
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]>
- 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]>
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]>
- 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]>
- 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]>
- 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]>
- 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]>
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]>
- 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]>
- 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]>
- 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]>
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]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
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]>
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]>