From b601a626f5555000bafd4d5c20cc34e30095d14a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 29 Apr 2026 14:54:15 -0400 Subject: [PATCH] docs: storage entries and migration guide for #6814 Updates persistent-storage.md to lead with the named storage entry workflow while keeping the legacy colon-form documentation intact, adds a Persistent storage section to the k3s scheduler doc, documents the storage-app-mounts, storage-create, storage-destroy, and storage-status triggers in plugin-triggers, and adds an entry to the 0.38.0 migration guide explaining the install-time migration of legacy mounts and the new DNS-1123 name validation. Bats coverage in tests/unit/storage.bats now exercises storage:create / list-entries / destroy, name validation rejections, multi-entry attachment, the destroy-while-mounted error, and the ensure-directory deprecation warning. --- docs/advanced-usage/persistent-storage.md | 33 ++++--- docs/appendices/0.38.0-migration-guide.md | 1 + docs/deployment/schedulers/k3s.md | 21 +++++ docs/development/plugin-triggers.md | 28 ++++++ tests/unit/storage.bats | 104 ++++++++++++++++++++++ 5 files changed, 177 insertions(+), 10 deletions(-) diff --git a/docs/advanced-usage/persistent-storage.md b/docs/advanced-usage/persistent-storage.md index d2289976a..c033e734e 100644 --- a/docs/advanced-usage/persistent-storage.md +++ b/docs/advanced-usage/persistent-storage.md @@ -1,24 +1,37 @@ # Persistent Storage > [!IMPORTANT] -> New as of 0.5.0 +> New as of 0.5.0. Named storage entries new as of 0.38.0. -The preferred method to mount external containers to a Dokku managed container, is to use the Dokku storage plugin. +The preferred method to attach persistent storage to a Dokku-managed container is the Dokku storage plugin. ``` -storage:ensure-directory [--chown option] # Creates a persistent storage directory in the recommended storage path -storage:list [--format text|json] # List bind mounts for app's container(s) (host:container) -storage:mount # Create a new bind mount -storage:report [] [] # Displays a checks report for one or more apps -storage:unmount # Remove an existing bind mount +storage:create [] [flags] # Register a named storage entry +storage:destroy # Remove a named storage entry (must be unmounted from every app first) +storage:ensure-directory [--chown option] # [DEPRECATED] use storage:create instead +storage:exec [-- ...] # Run a command (or shell) in a temporary container that mounts the entry +storage:info [--format text|json] # Show details for one storage entry +storage:list [--format text|json] # List bind mounts for an app's container(s) (legacy host:container view) +storage:list-entries [--scheduler s] [--format text|json] # List registered storage entries +storage:mount --container-dir [flags] # Mount a named entry into an app +storage:mount # [LEGACY] colon-form mount, docker-local only +storage:report [] [] # Display a storage report for one or more apps +storage:report --global # Display a cluster-wide entry inventory +storage:set [flags] # Update a storage entry in place +storage:unmount [--container-dir ] # Remove an attachment +storage:wait # Block until a k3s entry's PVC is bound ``` -> The storage plugin is compatible with storage mounts created with the docker-options. The storage plugin will only list mounts from the deploy/run phase. +A storage entry is the source of truth for the underlying volume - a host directory on docker-local, or a PersistentVolumeClaim on k3s. Multiple apps can mount the same entry, and an attachment carries the per-app details (container path, phases, subpath, readonly, process type). Names are globally unique across the install and must be DNS-1123 labels (lowercase letters, digits, dashes) of 45 characters or less so they can be used verbatim as Helm release and PVC names. + +The legacy `storage:mount :` form continues to work on docker-local. On a k3s app it is rejected; create a named entry with `storage:create --scheduler k3s` and mount it instead. Existing colon-form mounts are migrated automatically the first time the new code runs - they show up as `legacy-` entries in `storage:list-entries`. The storage plugin supports the following mount points: -- explicit paths that exist on the host -- docker volumes +- explicit paths that exist on the host (docker-local) +- docker volumes (docker-local) +- PersistentVolumeClaims provisioned via a StorageClass (k3s) +- hostPath-backed PVs (k3s) ## Usage diff --git a/docs/appendices/0.38.0-migration-guide.md b/docs/appendices/0.38.0-migration-guide.md index dbf297da8..96f0bfbfd 100644 --- a/docs/appendices/0.38.0-migration-guide.md +++ b/docs/appendices/0.38.0-migration-guide.md @@ -22,6 +22,7 @@ - The `docker-local` scheduler no longer queues an image for retirement when another running container of the same app still uses it. This fixes the case where a `ps:rebuild` against an image-based deploy (`git:from-image`) produced an identical-SHA image and the `dokku-retire` cron timer would log `Image ... has running containers, skipping rm` on every run. Stuck entries from prior versions are pruned automatically on the next `ps:retire` run. - All `:report` subcommands now accept the `--global` flag, which scopes the report to globally-configured properties. The flag composes with `--format json`, so a JSON report of global properties can be obtained via, for example, `dokku scheduler:report --global --format json`. Previously, combining `--global` with `--format json` was rejected with an "info flag" error, and `--global` on its own was treated as an unknown flag. - The `scheduler-k3s` plugin now manages env config and the dokku-generated image pull Secret as their own helm releases with stable names (`config-{app}` and `pull-secret-{app}`) rather than bundling them into the app helm chart with a per-deploy timestamp suffix (`env-{app}.{ts}` / `ims-{app}.{ts}`). This fixes two bugs: a helm rollback of the app chart no longer deletes Secrets that older ReplicaSets still reference, and the Deployment's `imagePullSecrets` list no longer accumulates references to nonexistent Secrets across deploys. The next deploy of an app switches the Deployment's `envFrom` and `imagePullSecrets` references to the stable names and prunes any leaked entries; existing live Deployments do not need to be patched manually. App rename now also uninstalls the old `tls-{app}`, `config-{app}`, and `pull-secret-{app}` releases under the previous app name; the new name's releases are recreated on the next deploy or certs sync. +- The storage plugin now treats persistent volumes as named, scheduler-aware first-class resources via `storage:create`, `storage:mount`, `storage:set`, and `storage:destroy`. The legacy `storage:mount :` colon form continues to work on docker-local apps but is deprecated; on k3s apps it is rejected. Existing colon-form mounts are migrated automatically the first time the new storage plugin runs (during the install trigger) - they appear as `legacy-` entries in `storage:list-entries`. The migration is idempotent and tied to a per-app flag file at `$DOKKU_LIB_ROOT/config/storage/.migrated/`; deleting that file forces a re-scan on the next install. The `storage:ensure-directory` command keeps working but now emits a deprecation warning - prefer `storage:create []` (the path defaults to the same `$DOKKU_LIB_ROOT/data/storage/` location). Storage entry names must now be DNS-1123 labels of 45 characters or less so they can be used verbatim as Helm release and Kubernetes resource names; underscores and uppercase characters that the older `ensure-directory` validator accepted are rejected for new names. The migration synthesizer always uses lowercase hex hashes so existing data is never locked out. ### TLS handshake behavior change diff --git a/docs/deployment/schedulers/k3s.md b/docs/deployment/schedulers/k3s.md index 33e55126b..b3fa64432 100644 --- a/docs/deployment/schedulers/k3s.md +++ b/docs/deployment/schedulers/k3s.md @@ -685,6 +685,27 @@ Alternatively, a comma separated list of chart names can be specified to only fo dokku scheduler-k3s:ensure-charts --charts cert-manager ``` +### Persistent storage + +K3s apps mount persistent volumes through the Dokku storage plugin. A `storage:create --scheduler k3s` call provisions a PersistentVolumeClaim through a small per-entry helm release; `storage:mount --container-dir ` then attaches it to an app. Because storage entries are global, the same PVC can be mounted into multiple apps (for `ReadWriteMany` workloads) or into the same app at multiple subpaths. + +```shell +# dynamically-provisioned PVC backed by Longhorn +dokku storage:create demo-data \ + --scheduler k3s \ + --size 2Gi \ + --access-mode ReadWriteOnce \ + --storage-class-name longhorn + +dokku storage:mount demo demo-data --container-dir /data +dokku storage:wait demo-data +git push dokku master +``` + +For a hostPath-backed PV (no StorageClass), pass `` as the second positional argument and omit `--storage-class-name`. The plugin renders both the PV and the PVC into the entry's helm release. The `--reclaim-policy` flag (`Retain` or `Delete`) controls whether the underlying PV survives `storage:destroy`. Annotations and labels on `storage:create` / `storage:set` propagate to both the PVC and the PV so backup tools (Velero, Longhorn snapshots) can find them. + +The legacy `storage:mount :` colon form is rejected on k3s apps; create a named entry instead. See [Persistent Storage](/docs/advanced-usage/persistent-storage.md) for the full command reference. + ### Chart upgrade callbacks Some chart version transitions require side-effects that a plain `helm upgrade` cannot perform. For example, the upgrade of `keda-add-ons-http` to `0.12.2` introduces breaking changes to deployment selectors, which are immutable in Kubernetes; the chart-managed deployments must be deleted before the upgrade can proceed. diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index 7fd0f7f29..c0dae2518 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -2920,6 +2920,34 @@ APP="$1" # TODO ``` +### `storage-app-mounts` + +- Description: Returns the (entry, attachment) pairs an app has for the given phase, in JSON. Schedulers consume this to render volume mounts. +- Invoked by: scheduler plugins during deploy +- Arguments: `$APP $PHASE` +- Output: a JSON array of objects shaped `{"entry": Entry, "attachment": Attachment}` + +### `storage-create` + +- Description: Provisions or updates the underlying volume for a k3s storage entry. Called when `storage:create` or `storage:set` runs against a k3s entry. Receives the JSON-encoded entry on stdin. +- Invoked by: `dokku storage:create`, `dokku storage:set` +- Arguments: `$ENTRY_NAME` +- Stdin: JSON-encoded `Entry` payload + +### `storage-destroy` + +- Description: Releases the underlying volume for a k3s storage entry. Called when `storage:destroy` runs against a k3s entry. Receives the JSON-encoded entry on stdin. +- Invoked by: `dokku storage:destroy` +- Arguments: `$ENTRY_NAME` +- Stdin: JSON-encoded `Entry` payload + +### `storage-status` + +- Description: Returns the current status of a storage entry's underlying volume. Used by `storage:wait` to poll the cluster. The handler should print one line: `Bound`, `Pending`, or `Lost` for k3s entries. +- Invoked by: `dokku storage:wait` +- Arguments: `$ENTRY_NAME` +- Stdin: JSON-encoded `Entry` payload + ### `traefik-template-source` - Description: Retrieves an alternative template for the traefik compose config diff --git a/tests/unit/storage.bats b/tests/unit/storage.bats index 9687d0e73..7c4b3ecc4 100644 --- a/tests/unit/storage.bats +++ b/tests/unit/storage.bats @@ -183,3 +183,107 @@ teardown() { assert_success assert_output "mount_volume:/mount" } + +@test "(storage) storage:create / storage:list-entries / storage:destroy" { + run /bin/bash -c "dokku storage:create rdmtest-entry" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku storage:list-entries --format json | jq -r '.[].name' | grep '^rdmtest-entry$'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "rdmtest-entry" + + run /bin/bash -c "dokku storage:info rdmtest-entry --format json | jq -r '.scheduler'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "docker-local" + + run /bin/bash -c "dokku storage:destroy rdmtest-entry" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(storage) storage:create rejects invalid names" { + # underscore: rejected + run /bin/bash -c "dokku storage:create rdmtest_invalid" + echo "output: $output" + echo "status: $status" + assert_failure + + # uppercase: rejected + run /bin/bash -c "dokku storage:create RdmTest" + echo "output: $output" + echo "status: $status" + assert_failure + + # 46 chars: too long + long_name=$(printf 'a%.0s' {1..46}) + run /bin/bash -c "dokku storage:create $long_name" + echo "output: $output" + echo "status: $status" + assert_failure + + # legacy- prefix: reserved + run /bin/bash -c "dokku storage:create legacy-foo" + echo "output: $output" + echo "status: $status" + assert_failure +} + +@test "(storage) storage:create + storage:mount with named entry attaches multiple entries to one app" { + run /bin/bash -c "dokku storage:create rdmtest-data" + assert_success + run /bin/bash -c "dokku storage:create rdmtest-cache" + assert_success + + run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-data --container-dir /data" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-cache --container-dir /cache" + echo "output: $output" + echo "status: $status" + assert_success + + # cleanup + run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-data" + assert_success + run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-cache" + assert_success + run /bin/bash -c "dokku storage:destroy rdmtest-data" + assert_success + run /bin/bash -c "dokku storage:destroy rdmtest-cache" + assert_success +} + +@test "(storage) storage:destroy refuses to remove a still-mounted entry" { + run /bin/bash -c "dokku storage:create rdmtest-busy" + assert_success + run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-busy --container-dir /data" + assert_success + + run /bin/bash -c "dokku storage:destroy rdmtest-busy" + echo "output: $output" + echo "status: $status" + assert_failure + assert_output_contains "still mounted" + + run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-busy" + assert_success + run /bin/bash -c "dokku storage:destroy rdmtest-busy" + assert_success +} + +@test "(storage) storage:ensure-directory emits deprecation warning" { + run /bin/bash -c "dokku storage:ensure-directory $TEST_APP 2>&1" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "deprecated" +}