From bf26539949c807b25e86745dd66b76a5a2a88605 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 23 Sep 2026 01:10:49 -0400 Subject: [PATCH] feat: allow storage:mount --replace to vary fields per mount `storage:mount --replace` writes one process type's whole attachment set in a single call, but only part of an attachment could vary between the mounts in that call: each mount carried its own `ro` and Docker mount options while `--phase`, `--volume-subpath` and `--volume-chown` were fixed for the whole call, so an app whose mounts differ only in subpath still needed one call per mount, which is the non-atomic convergence `--replace` exists to remove. Each mount's option list now also carries `phase=`, `volume-subpath=` and `volume-chown=` tokens alongside `ro`, its new opposite `rw` and the Docker options it already passed through, and so that each attribute has exactly one spelling the five matching flags are now rejected with `--replace` - one of which, `--volume-options`, was previously accepted and silently dropped. `--process-type` still scopes the whole call, since it selects which mounts the call is authoritative over rather than describing any one of them. Every mount is parsed and validated before anything is written, so a rejected token leaves the stored mounts untouched. The documentation added for `--replace` was also marked as new in 0.38.28 in four places, but it shipped in 0.38.29. --- docs/advanced-usage/persistent-storage.md | 41 ++- plugins/storage/mounts_test.go | 340 ++++++++++++++++-- plugins/storage/mountspec.go | 164 +++++++++ .../storage/src/subcommands/subcommands.go | 10 +- plugins/storage/subcommands.go | 75 ++-- tests/unit/storage-3.bats | 157 +++++++- 6 files changed, 699 insertions(+), 88 deletions(-) create mode 100644 plugins/storage/mountspec.go diff --git a/docs/advanced-usage/persistent-storage.md b/docs/advanced-usage/persistent-storage.md index a3488bc99..d7884d758 100644 --- a/docs/advanced-usage/persistent-storage.md +++ b/docs/advanced-usage/persistent-storage.md @@ -271,7 +271,7 @@ dokku ps:restart app-name #### Scoping a mount to a process type > [!IMPORTANT] -> New as of 0.38.28 +> New as of 0.38.29 By default a mount is bound into every one of an app's containers. `--process-type` narrows it to a single Procfile process type, which is how a worker gets a scratch volume the web process has no business seeing: @@ -305,7 +305,7 @@ dokku storage:mount node-js-app node-js-cache --container-dir /app/storage --pro #### Replacing the entire mount set > [!IMPORTANT] -> New as of 0.38.28 +> New as of 0.38.29. Per-mount attributes in the option list new as of 0.38.30. The form above mounts one entry at a time, so matching an app's mounts to a declared set means reading `storage:list`, computing the difference in both directions and issuing one `storage:mount` or `storage:unmount` per addition and removal - with a failure partway through leaving the app holding a mixture of the two sets. The `--replace` flag writes the whole set in a single call, taking `name:container-dir` pairs instead of a single entry and a `--container-dir` flag. @@ -313,30 +313,47 @@ The form above mounts one entry at a time, so matching an app's mounts to a decl dokku storage:mount --replace node-js-app node-js-data:/app/storage node-js-cache:/cache ``` -Anything previously mounted and not named in the call is unmounted. Each pair takes the same optional third field as the legacy colon form, so `ro` and other mount options can vary between mounts: +Anything previously mounted and not named in the call is unmounted. Each pair takes the same optional third field as the legacy colon form, a comma-separated list, and that list carries every mount-time attribute the mount needs, so two mounts declared in one call can differ in all of them: ```shell -dokku storage:mount --replace node-js-app node-js-data:/app/storage:Z node-js-cache:/cache:ro,noexec +dokku storage:mount --replace node-js-app \ + node-js-data:/app/storage:ro,Z,volume-subpath=uploads,volume-chown=herokuish \ + node-js-cache:/cache:phase=deploy,phase=run,noexec ``` -A pair whose first field starts with `/` is a legacy host path rather than an entry name, and registers its `legacy-` entry the same way the colon form does. Any other first field must name a registered entry, so a mistyped name is rejected rather than taken for a docker volume. A docker volume already mounted through the colon form is named here by the `legacy-` entry that `storage:list-entries` shows. +| Token | Meaning | +|---|---| +| `ro` | Mount the volume read-only | +| `rw` | Mount it read-write, the explicit opposite of `ro` and the default | +| `volume-subpath=` | Subpath within the entry | +| `volume-chown=