Files
dokku/tests/unit/storage-1.bats
Jose Diaz-Gonzalez 74f9acf7ba refactor: align storage:set with dokku conventions
`storage:set` now takes `<name> <property> [<value>]` like every other `:set` command, where omitting the value unsets the property. Previously it took flags and could not distinguish an empty value from an omitted one, so nothing it set could ever be cleared. The flag form keeps working and emits a deprecation warning.

Annotations and labels move to `storage:annotations:set`, `storage:annotations:report`, `storage:labels:set`, and `storage:labels:report`, matching the `scheduler-k3s` equivalents. These operate on a single key, so clearing one leaves the rest in place rather than replacing the whole map as the `--annotation` and `--label` flags do.
2026-08-10 15:00:45 -04:00

833 lines
30 KiB
Bash

#!/usr/bin/env bats
load test_helper
setup() {
global_setup
create_app
rm -rf "$DOKKU_LIB_ROOT/data/storage/rdmtestapp*"
}
teardown() {
destroy_app
global_teardown
}
@test "(storage) storage:help" {
run /bin/bash -c "dokku storage"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage mounted volumes"
help_output="$output"
run /bin/bash -c "dokku storage:help"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage mounted volumes"
assert_output "$help_output"
}
@test "(storage) storage:ensure-directory" {
run /bin/bash -c "test -d $DOKKU_LIB_ROOT/data/storage/$TEST_APP"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku storage:ensure-directory @"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku storage:ensure-directory $TEST_APP/"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku storage:ensure-directory $TEST_APP/$TEST_APP"
echo "output: $output"
echo "status: $status"
assert_failure
run /bin/bash -c "dokku storage:ensure-directory $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "Setting directory ownership to 1000:1000" 0
assert_output_contains "Setting directory ownership to 2000:2000" 0
assert_output_contains "Setting directory ownership to 32767:32767" 1
run /bin/bash -c "test -d $DOKKU_LIB_ROOT/data/storage/$TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:ensure-directory $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:ensure-directory --chown false $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "Setting directory ownership to 1000:1000" 0
assert_output_contains "Setting directory ownership to 2000:2000" 0
assert_output_contains "Setting directory ownership to 32767:32767" 0
run /bin/bash -c "dokku storage:ensure-directory $TEST_APP --chown false"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "Setting directory ownership to 1000:1000" 0
assert_output_contains "Setting directory ownership to 2000:2000" 0
assert_output_contains "Setting directory ownership to 32767:32767" 0
run /bin/bash -c "dokku storage:ensure-directory --chown heroku $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "Setting directory ownership to 1000:1000" 1
assert_output_contains "Setting directory ownership to 2000:2000" 0
assert_output_contains "Setting directory ownership to 32767:32767" 0
run /bin/bash -c "dokku storage:ensure-directory --chown paketo $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "Setting directory ownership to 1000:1000" 0
assert_output_contains "Setting directory ownership to 2000:2000" 1
assert_output_contains "Setting directory ownership to 32767:32767" 0
run /bin/bash -c "dokku storage:ensure-directory --chown herokuish $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "Setting directory ownership to 1000:1000" 0
assert_output_contains "Setting directory ownership to 2000:2000" 0
assert_output_contains "Setting directory ownership to 32767:32767" 1
}
@test "(storage) chown-storage-dir rejects out-of-bounds chown values" {
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir $TEST_APP 65536"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Unsupported chown permissions"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir $TEST_APP 165535"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Unsupported chown permissions"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir $TEST_APP 231072"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Unsupported chown permissions"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir $TEST_APP -1"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Value must be a non-negative integer"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chown-storage-dir $TEST_APP abc"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Value must be a non-negative integer"
}
@test "(storage) storage:mount, storage:list, storage:umount" {
run /bin/bash -c "dokku storage:mount $TEST_APP /tmp/mount:/mount"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet storage:list $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "/tmp/mount:/mount"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "1"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].host_path'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "/tmp/mount"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].container_path'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "/mount"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r 'map(has(\"volume_options\") or has(\"readonly\")) | any'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
run /bin/bash -c "dokku storage:mount $TEST_APP /tmp/mount:/mount"
echo "output: $output"
echo "status: $status"
assert_output " ! Mount path already exists."
assert_failure
run /bin/bash -c "dokku storage:unmount $TEST_APP /tmp/mount:/mount"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet storage:list $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_not_exists
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "0"
run /bin/bash -c "dokku storage:unmount $TEST_APP /tmp/mount:/mount"
echo "output: $output"
echo "status: $status"
assert_output " ! Mount path does not exist."
assert_failure
run /bin/bash -c "dokku storage:mount $TEST_APP mount_volume:/mount"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet storage:list $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "mount_volume:/mount"
}
@test "(storage:report) build/deploy/run mounts raw" {
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-build-mounts\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-deploy-mounts\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-run-mounts\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '[keys[] | select(startswith(\"attachment.\"))] | length'"
assert_success
assert_output "0"
run /bin/bash -c "dokku storage:mount $TEST_APP /tmp/storage-mount:/mount"
assert_success
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-deploy-mounts\"'"
assert_success
assert_output "-v /tmp/storage-mount:/mount"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-run-mounts\"'"
assert_success
assert_output "-v /tmp/storage-mount:/mount"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-build-mounts\"'"
assert_success
assert_output ""
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '[keys[] | select(startswith(\"attachment.\"))] | length'"
assert_success
assert_output "9"
run /bin/bash -c "dokku storage:unmount $TEST_APP /tmp/storage-mount:/mount"
assert_success
}
@test "(storage:report) emits per-attachment dotted keys" {
run /bin/bash -c "dokku storage:create rdmtest-rpt"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-rpt --container-dir /data --phase deploy --phase run --volume-options Z --volume-chown herokuish --volume-subpath uploads"
assert_success
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.entry-name\"'"
assert_success
assert_output "rdmtest-rpt"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.host-path\"'"
assert_success
assert_output "$DOKKU_LIB_ROOT/data/storage/rdmtest-rpt"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.container-path\"'"
assert_success
assert_output "/data"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.phases\"'"
assert_success
assert_output "deploy,run"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.process-type\"'"
assert_success
assert_output "_default_"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.subpath\"'"
assert_success
assert_output "uploads"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.readonly\"'"
assert_success
assert_output "false"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.volume-options\"'"
assert_success
assert_output "Z"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"attachment.1.volume-chown\"'"
assert_success
assert_output "herokuish"
# stdout output gains one line per field; verify a representative subset
run /bin/bash -c "dokku storage:report $TEST_APP"
assert_success
assert_output_contains "Storage attachment 1 entry name"
assert_output_contains "rdmtest-rpt" -1
assert_output_contains "Storage attachment 1 volume options"
assert_output_contains "Storage attachment 1 volume chown"
assert_output_contains "herokuish"
# info-flag lookup returns just the value
run /bin/bash -c "dokku storage:report $TEST_APP --storage-attachment.1.volume-options"
assert_success
assert_output "Z"
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-rpt --container-dir /data"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-rpt --force"
assert_success
}
@test "(storage:report) multiple attachments cluster by index" {
run /bin/bash -c "dokku storage:create rdmtest-rpt-a"
assert_success
run /bin/bash -c "dokku storage:create rdmtest-rpt-b"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-rpt-a --container-dir /data --volume-options Z"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-rpt-b --container-dir /cache --volume-options noexec,nosuid"
assert_success
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '[.\"attachment.1.entry-name\", .\"attachment.2.entry-name\"] | sort | join(\"|\")'"
assert_success
assert_output "rdmtest-rpt-a|rdmtest-rpt-b"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '[.\"attachment.1.volume-options\", .\"attachment.2.volume-options\"] | sort | join(\"|\")'"
assert_success
assert_output "Z|noexec,nosuid"
# info-flag lookup against the second attachment
run /bin/bash -c "dokku storage:report $TEST_APP --storage-attachment.2.entry-name"
assert_success
assert_output_contains "rdmtest-rpt"
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-rpt-a --container-dir /data"
assert_success
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-rpt-b --container-dir /cache"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-rpt-a --force"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-rpt-b --force"
assert_success
}
@test "(storage:report) degrades gracefully when an attachment entry is missing" {
run /bin/bash -c "dokku storage:create rdmtest-rpt-missing"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-rpt-missing --container-dir /data --volume-options Z"
assert_success
# delete the entry on disk so LoadEntry fails for the existing attachment
run /bin/bash -c "sudo rm -f $DOKKU_LIB_ROOT/data/storage-registry/entries/rdmtest-rpt-missing.json"
assert_success
# discard dokku's stderr so the LogWarn line doesn't pollute the jq output capture
run /bin/bash -c "dokku storage:report $TEST_APP --format json 2>/dev/null | jq -r '[keys[] | select(startswith(\"attachment.\"))] | length'"
assert_success
assert_output "0"
run /bin/bash -c "dokku storage:report $TEST_APP --format json 2>/dev/null | jq -r 'has(\"storage-build-mounts\") and has(\"storage-deploy-mounts\") and has(\"storage-run-mounts\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku storage:report $TEST_APP"
assert_success
assert_output_contains "Skipping attachment"
assert_output_contains "rdmtest-rpt-missing" -1
assert_output_contains "Storage build mounts"
assert_output_contains "Storage deploy mounts"
assert_output_contains "Storage run mounts"
# clean up the leftover attachment record so subsequent tests don't trip
run /bin/bash -c "sudo rm -f $DOKKU_LIB_ROOT/config/storage/$TEST_APP/attachments"
assert_success
}
@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
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "2"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].entry_name' | sort | xargs"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "rdmtest-cache rdmtest-data"
# 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:list $TEST_APP --format json | jq -r '. | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "0"
run /bin/bash -c "dokku storage:destroy rdmtest-data --force"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-cache --force"
assert_success
}
@test "(storage:mount) --volume-options sets option on named-entry attachment" {
run /bin/bash -c "dokku storage:create rdmtest-opts"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-opts --container-dir /data --volume-options Z"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].volume_options'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "Z"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].container_path'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "/data"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-deploy-mounts\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains ":Z"
# combine with --volume-readonly: rendered as ro,<options>
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-opts --container-dir /ro --volume-options noexec,nosuid --volume-readonly"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r '.\"storage-deploy-mounts\"'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains ":ro,noexec,nosuid"
# The JSON output of storage:list exposes readonly and volume_options
# as separate keys so external drift-detection tooling can compare
# them against the underlying attachment fields one for one.
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[] | select(.container_path == \"/ro\") | .readonly'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[] | select(.container_path == \"/ro\") | .volume_options'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "noexec,nosuid"
# The /data mount has no readonly flag, so the key is absent.
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[] | select(.container_path == \"/data\") | has(\"readonly\")'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
# cleanup
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-opts --container-dir /data"
assert_success
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-opts --container-dir /ro"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-opts --force"
assert_success
}
@test "(storage:mount) re-running against an existing (entry, container-dir) updates in place" {
run /bin/bash -c "dokku storage:create rdmtest-upsert"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-upsert --container-dir /data --volume-options Z"
echo "output: $output"
echo "status: $status"
assert_success
# Re-mount the same (entry, container-dir) with a different --volume-options.
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-upsert --container-dir /data --volume-options noexec,nosuid"
echo "output: $output"
echo "status: $status"
assert_success
# Single attachment, not two.
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "1"
# Latest --volume-options wins.
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].volume_options'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "noexec,nosuid"
# Re-mount with --volume-readonly added; readonly toggles on.
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-upsert --container-dir /data --volume-options noexec,nosuid --volume-readonly"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[].readonly'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "true"
# Re-mount without --volume-readonly: mount-time fields are rewritten,
# not merged, so the readonly key disappears from the JSON output.
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-upsert --container-dir /data --volume-options noexec,nosuid"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '.[] | has(\"readonly\")'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "false"
# A different --container-dir on the same entry is still a distinct attachment.
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-upsert --container-dir /other"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "2"
# cleanup
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-upsert --container-dir /data"
assert_success
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-upsert --container-dir /other"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-upsert --force"
assert_success
}
@test "(storage) storage:exec runs a non-interactive command and propagates exit code" {
run /bin/bash -c "dokku storage:create rdmtest-exec"
assert_success
# docker pull image up-front so the test isn't gated on registry latency.
run /bin/bash -c "docker pull alpine:3 >/dev/null"
assert_success
run /bin/bash -c "dokku storage:exec rdmtest-exec -- /bin/sh -c 'touch /data/marker && ls /data/marker'"
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "/data/marker"
run /bin/bash -c "dokku storage:exec rdmtest-exec -- /bin/sh -c 'exit 42'"
echo "output: $output"
echo "status: $status"
assert_equal "$status" 42
run /bin/bash -c "dokku storage:destroy rdmtest-exec --force"
assert_success
}
@test "(storage) storage:migrate converts legacy docker-options -v lines into attachments" {
# Stage a legacy `-v` line directly via docker-options:add. This
# bypasses the new storage:mount path so the line lives in
# docker-options without a corresponding attachment, simulating an
# app upgraded from a pre-PR Dokku version.
run /bin/bash -c "dokku docker-options:add $TEST_APP deploy,run \"-v /tmp/legacy-mount:/legacy\""
echo "output: $output"
echo "status: $status"
assert_success
# Before migration, storage:list shows nothing (attachment-only) and
# docker-options has the -v line.
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
assert_success
assert_output "0"
run /bin/bash -c "dokku docker-options:report $TEST_APP --docker-options-deploy"
assert_success
assert_output_contains "-v /tmp/legacy-mount:/legacy"
# Run the migration for this app.
run /bin/bash -c "dokku storage:migrate $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
# storage:list now surfaces the synthesized colon form.
run /bin/bash -c "dokku --quiet storage:list $TEST_APP"
assert_success
assert_output "/tmp/legacy-mount:/legacy"
# The synthesized entry shows up under the legacy- prefix.
run /bin/bash -c "dokku storage:list-entries --format json | jq -r '.[].name' | grep '^legacy-' | head -1"
echo "output: $output"
echo "status: $status"
assert_success
# Regression for #8557: the legacy-*.json file must be readable by the
# dokku user. The migration runs as root via the install trigger, so
# without an explicit chown the file lands as root:root and ps:rebuild
# fails with permission denied.
legacy_entry_name=$(dokku storage:list-entries --format json | jq -r '.[] | select(.name | startswith("legacy-")) | .name' | head -1)
run /bin/bash -c "stat -c '%U:%G' /var/lib/dokku/data/storage-registry/entries/${legacy_entry_name}.json"
assert_success
assert_output "dokku:dokku"
# docker-options no longer holds the -v line on either phase.
run /bin/bash -c "dokku docker-options:report $TEST_APP --docker-options-deploy"
assert_success
assert_output_not_contains "-v /tmp/legacy-mount:/legacy"
run /bin/bash -c "dokku docker-options:report $TEST_APP --docker-options-run"
assert_success
assert_output_not_contains "-v /tmp/legacy-mount:/legacy"
# Idempotency: re-running storage:migrate is a no-op (still one
# attachment, no errors).
run /bin/bash -c "dokku storage:migrate $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku storage:list $TEST_APP --format json | jq -r '. | length'"
assert_success
assert_output "1"
# Property marker is set after a successful migration with mounts.
run /bin/bash -c "sudo test -f /var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
assert_success
run /bin/bash -c "sudo cat /var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
assert_success
assert_output "true"
# Cleanup: unmount + destroy the synthesized legacy entry. We need
# the entry name from list-entries since legacy-<hash> is content-derived.
legacy_name=$(dokku storage:list-entries --format json | jq -r '.[] | select(.name | startswith("legacy-")) | .name' | head -1)
if [[ -n "$legacy_name" ]]; then
run /bin/bash -c "dokku storage:unmount $TEST_APP $legacy_name --container-dir /legacy"
assert_success
run /bin/bash -c "dokku storage:destroy $legacy_name --force"
assert_success
fi
}
@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:"
}
@test "(storage:migrate) leaves legacy-mounts-migrated unset for apps with no legacy -v lines" {
# Fresh app with no -v lines: storage:migrate must NOT write the
# legacy-mounts-migrated property. Distinguishes "never had legacy
# state" from "had legacy state, drained".
sudo rm -f "/var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
run /bin/bash -c "dokku storage:migrate $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "sudo test -f /var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
assert_failure
}
@test "(storage:migrate) converts legacy migration flag file into property" {
# Stage an upgrade-from-old-code scenario: a leftover flag file under
# data/storage-registry/migrations/<app>. storage:migrate (which runs
# convertLegacyMigrationFlag before migrateApp) must drain it into
# the property and remove the file.
flag_dir="/var/lib/dokku/data/storage-registry/migrations"
sudo mkdir -p "$flag_dir"
sudo touch "$flag_dir/$TEST_APP"
sudo chown dokku:dokku "$flag_dir/$TEST_APP"
sudo rm -f "/var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
run /bin/bash -c "dokku storage:migrate $TEST_APP"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "sudo test -f /var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
assert_success
run /bin/bash -c "sudo cat /var/lib/dokku/config/storage/$TEST_APP/legacy-mounts-migrated"
assert_success
assert_output "true"
run /bin/bash -c "sudo test -e $flag_dir/$TEST_APP"
assert_failure
}
@test "(storage:report) emits new stripped JSON keys alongside legacy" {
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r 'has(\"build-mounts\") and has(\"storage-build-mounts\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r 'has(\"deploy-mounts\") and has(\"storage-deploy-mounts\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r 'has(\"run-mounts\") and has(\"storage-run-mounts\")'"
assert_success
assert_output "true"
# per-attachment dotted keys are emitted in both stripped and legacy forms too
run /bin/bash -c "dokku storage:create rdmtest-rpt-keys"
assert_success
run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-rpt-keys --container-dir /data"
assert_success
run /bin/bash -c "dokku storage:report $TEST_APP --format json | jq -r 'has(\"attachment.1.entry-name\") and has(\"storage-attachment.1.entry-name\")'"
assert_success
assert_output "true"
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-rpt-keys --container-dir /data"
assert_success
run /bin/bash -c "dokku storage:destroy rdmtest-rpt-keys --force"
assert_success
}
@test "(storage) chmod-storage-dir rejects invalid modes" {
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chmod-storage-dir $TEST_APP 0888"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Unsupported directory mode"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chmod-storage-dir $TEST_APP 07555"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Unsupported directory mode"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chmod-storage-dir $TEST_APP u+rwx"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Unsupported directory mode"
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/chmod-storage-dir '../escape' 0777"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Directory can only contain the following set of characters"
}
@test "(storage) destroy-storage-dir refuses a traversing directory name" {
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/destroy-storage-dir '../escape'"
echo "output: $output"
echo "status: $status"
assert_failure
assert_output_contains "Directory can only contain the following set of characters"
# a missing directory is a no-op rather than an error
run /bin/bash -c "DOKKU_LIB_ROOT=$DOKKU_LIB_ROOT $PLUGIN_AVAILABLE_PATH/storage/bin/destroy-storage-dir rdmtest-absent"
echo "output: $output"
echo "status: $status"
assert_success
}
@test "(storage) install trigger whitelists the storage directory helpers" {
run /bin/bash -c "test -f /etc/sudoers.d/dokku-storage"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "sudo grep -c 'storage/bin/chown-storage-dir' /etc/sudoers.d/dokku-storage"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "1"
run /bin/bash -c "sudo grep -c 'storage/bin/chmod-storage-dir' /etc/sudoers.d/dokku-storage"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "1"
run /bin/bash -c "sudo grep -c 'storage/bin/destroy-storage-dir' /etc/sudoers.d/dokku-storage"
echo "output: $output"
echo "status: $status"
assert_success
assert_output "1"
}