Repository navigation
Conversation
ndeloof
force-pushed
the
mounts-attribute
branch
from
October 30, 2026 13:15
ed951e2 to
2bbaf17
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved mount-handling defects can discard job and hook volume settings, bypass validation and image resolution, mishandle paths, and change configuration hashes during round trips.
8 open issues
Convert bind sources in explicit mount entries · New Validate mount consistency for jobs and hooks · New Enforce mount target uniqueness across pre_start hooks · New Resolve nested volume driver device paths · New Retain volumes referenced by jobs and service hooks · New Resolve image mounts declared on pre_start hooks · New Allow supported named-volume bind options in short syntax · New Preserve omitted mount option blocks during round trips · New
2 files not reviewed
types/container_spec_attributes.gen.go: Generated filetypes/derived.gen.go: Generated file
What changed in this PR
This PR adds structured mounts to compose-go’s container model and makes short syntax the canonical YAML representation for volumes that can be expressed losslessly.
Changes:
- Adds mount types and integrates them with loading, interpolation, path resolution, merging, and validation.
- Moves volume parsing into
typesand adds opt-in conversion from volumes to mounts. - Adds tests and updates YAML expectations while retaining structured JSON serialization.
| File | Description |
|---|---|
types/volume_spec.go |
Relocates the short-syntax volume parser into types. |
types/volume_spec_test.go |
Moves parser tests alongside the implementation. |
types/volume_spec_fuzz_test.go |
Moves volume parser fuzz coverage. |
types/volume_short.go |
Adds expressibility checks and canonical YAML serialization. |
types/volume_short_test.go |
Tests short syntax and serialization. |
types/types.go |
Adds mounts to the shared container specification. |
types/project.go |
Integrates mounts into resource pruning and image resolution. |
types/project_test.go |
Tests mount resource retention and image resolution. |
types/mounts.go |
Defines mount types and volume-to-mount conversion. |
types/mounts_test.go |
Tests conversion and unsupported SELinux options. |
types/derived.gen.go |
Adds generated deep-copy support for mounts. |
types/container_spec_attributes.gen.go |
Registers the mounts attribute. |
transform/volume.go |
Adds mount transformation and shares tmpfs mode parsing. |
transform/canonical.go |
Registers mount canonicalization. |
schema/compose-spec.json |
Defines mounts and deprecates long volume syntax. |
paths/resolve.go |
Registers bind-source resolution for mounts. |
override/uncity.go |
Adds target-based mount deduplication. |
override/merge.go |
Registers mount volume-label merging. |
override/merge_service_mounts_test.go |
Tests mount override behavior. |
loader/validate.go |
Adds mount reference and target-conflict checks. |
loader/tests/volumes_test.go |
Tests canonical volume YAML and round trips. |
loader/tests/mounts_test.go |
Adds mount loading and validation coverage. |
loader/tests/mounts_convert_test.go |
Tests optional conversion and idempotence. |
loader/tests/completeness_test.go |
Registers mount conformance coverage. |
loader/testdata/golden/web-stack.golden.yaml |
Updates expected canonical volume YAML. |
loader/normalize.go |
Cleans mount targets and excludes mounts from hook inheritance. |
loader/mounts.go |
Implements project-wide optional volume conversion. |
loader/loader.go |
Adds and invokes the conversion option. |
loader/interpolate.go |
Registers mount boolean interpolation casts. |
format/volume.go |
Preserves the existing parser API through delegation. |
Review effort: Balanced
ndeloof
force-pushed
the
mounts-attribute
branch
2 times, most recently
from
November 2, 2026 09:02
c44a446 to
d6e4087
Compare
ndeloof
marked this pull request as ready for review
November 2, 2026 12:06
There was a problem hiding this comment.
🟡 Changes recommended
Volume short-form serialization still changes service JSON/hash on YAML round trips by dropping empty volume blocks or changing create_host_path from false to true.
1 open issue
8 resolved since last review
Preserve omitted mount option blocks during round trips Allow supported named-volume bind options in short syntax Resolve image mounts declared on pre_start hooks Retain volumes referenced by jobs and service hooks Resolve nested volume driver device paths Enforce mount target uniqueness across pre_start hooks Validate mount consistency for jobs and hooks Convert bind sources in explicit mount entries
2 files not reviewed
types/container_spec_attributes.gen.go: Generated filetypes/derived.gen.go: Generated file
Review effort: Balanced
Comment on lines
+154
to
+163
| func (v ServiceVolumeConfig) readsBackWithItsBlocks() bool { | ||
| switch v.Type { | ||
| case VolumeTypeVolume: | ||
| hasBindOptions := v.Bind != nil && (v.Bind.SELinux != "" || v.Bind.Propagation != "") | ||
| return v.Volume != nil && (v.Bind != nil) == hasBindOptions | ||
| case VolumeTypeBind: | ||
| return v.Bind != nil && v.Volume == nil | ||
| } | ||
| return false | ||
| } |
ShortSyntax renders a volume as `[SOURCE:]TARGET[:OPTIONS]` when some spelling of it parses back as the very same volume, and IsShortSyntaxExpressible reports it. A volume the short syntax has no place for (tmpfs, image, subpath, labels, consistency, extensions, a bind that doesn't create its host path or is recursive...) or would read otherwise (a colon in a path, a bind source that is not a path, a one letter volume name read as a drive) is not expressible, and can only be written in the long syntax. It lets an implementation decide, without guessing from each option, which volumes it can hand over as a plain string (the `Binds` of the Docker API) and which need more. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
`mounts` lists the mounts of a container (service, job, pre_start hook) with the structure of `docker run --mount`: unlike an entry of `volumes`, the implementation has no latitude on how a mount is made. It is the structure of the long syntax of `volumes`, but for three things: `bind.create_src` is off unless set, as `bind-create-src` is, where `create_host_path` is on unless set; `bind.selinux` is missing, which the flag has no equivalent for; and `volume.driver` and `volume.driver_opts` exist, as `volume-driver` and `volume-opt` do. There is no short syntax, which would bring back the ambiguity of a colon or a comma in a path. Mounts are canonicalized (tmpfs mode), have their bind source resolved against the project directory, their target cleaned, are merged by target as volumes are, are interpolated, and their target and volume are checked together with the ones of volumes. Image mounts have their image resolved, and a volume used by a mount is not an unnecessary resource. A pre_start hook doesn't inherit the mounts of its service, as it doesn't inherit its volumes. ServiceVolumeConfig.ToMount returns the mount that does what a volume does. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Options.ConvertVolumesToMounts moves, once the model is loaded, the volumes of services, jobs and pre_start hooks that the short syntax can't say to their mounts, as the mounts they are equivalent to. The volumes it can say stay, so that the step is idempotent, and what `config` prints loads as the same model. A volume that can't become a mount, because it sets the SELinux relabeling of a bind mount, is refused with the name of the service and of the volume rather than silently dropped. ConvertVolumesToMounts is exported for what adds volumes to a project after it was loaded. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
…long one A volume that the short syntax can say is now marshalled to YAML as a string (`/data:/t:ro`, `cache:/t:nocopy`), whatever the syntax it was declared with, and only the others keep the long syntax. With the volumes the short syntax can't say moved to `mounts`, the long syntax of `volumes` is left to what a mount does better: the schema marks it deprecated. What is written reads back as the same volume, which is checked by parsing the string back. The JSON form is left as it was: it is what the hash of a service is computed from, and what tools read the model from. The volume parser has to be known to `types` for that, so it moves there with the short syntax check; `format.ParseVolume` and `format.Propagations` stay as they were. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
ndeloof
force-pushed
the
mounts-attribute
branch
from
November 2, 2026 13:03
d6e4087 to
a571b4c
Compare
ndeloof
added a commit
to ndeloof/compose
that referenced
this pull request
Nov 4, 2026
Replace compose-go with ndeloof/compose-go at the head of compose-spec/compose-go#979 so that the next commit can exercise the `mounts` service attribute. To be dropped once that PR is released. Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What this PR does, in one sentence
Adds a
mountsattribute, structured likedocker run --mount, makes the short syntax the canonical form ofvolumes, and moves what the short syntax can't say tomounts.Context
volumeshas two syntaxes that don't say the same things. The short syntax ([SOURCE:]TARGET[:OPTIONS]) is whatdocker run -vreads, so it is bound by its limits: no colon in a path, nosubpath, no volume labels, nobind.create_host_path: false, no tmpfs or image mount. The long syntax goes beyond, but leaves it to the implementation to choose how the mount is made (bind string or Mount API), and the two are not strictly interchangeable. The result is that the samevolumesentry can mean slightly different things depending on what is written next to it.What the PR brings
mounts, on services, jobs andpre_starthooks: structured YAML only (no short or CSV syntax, since--mountreads,as a separator), with the options of the long syntax ofvolumes, plusvolume.driver/volume.driver_opts, andbind.create_src(off unless set, likebind-create-src). Entries are merged and made unique by target, relative paths are resolved, values are interpolated, and a target can't be declared by bothvolumesandmounts. Hooks don't inheritmounts.format.IsShortSyntaxExpressible, proposed on its own in format: tell whether a volume can be said in the short syntax #986 (the first commit of this branch is that PR, and goes away once it is merged). It is defined by a round trip (the volume rendered in the short syntax must parse back as the same volume), so it can't drift from the parser. This PR also makes it a method,ServiceVolumeConfig.IsShortSyntaxExpressible(), which the canonical form below needs: for that, the volume parser moves totypes;format.ParseVolume,format.Propagationsand theformatfunctions of format: tell whether a volume can be said in the short syntax #986 are unchanged.loader.Options.ConvertVolumesToMounts(andloader.ConvertVolumesToMounts(project)) moves the entries ofvolumesthat the short syntax can't say tomounts. It is idempotent, and fails with an explicit error forbind.selinuxon such an entry, which a mount has no equivalent for.volumes: an entry that the short syntax can say is marshalled to YAML as a string (/data:/t:ro,cache:/t:nocopy), whatever the syntax it was declared with, and the long syntax is deprecated in the schema in favor ofmounts. Existing files load as before.Guardrails: the JSON form of a volume is left structured, because it is what Compose computes the config hash of a service from, and what tools read the model from; the canonical form changes the YAML only (the golden JSON is unchanged). Without
ConvertVolumesToMounts, an entry that the short syntax can't say stays involumes, in the long syntax. The loaded model of existing files is unchanged.Not settled, and the reason for the draft: the spelling of
bind.create_src, whethertypehas to stay required in a mount (--mountdefaults to a volume), whetherbind.propagationandvolume.nocopyshould be exceptions to the rule since the short syntax can say them, and whetherConvertVolumesToMountsshould become the default. The matching change to the specification is still to be proposed.🤖 Generated with Claude Code