diff --git a/docs/advanced-usage/persistent-storage.md b/docs/advanced-usage/persistent-storage.md index 742872ecc..5f10a64b2 100644 --- a/docs/advanced-usage/persistent-storage.md +++ b/docs/advanced-usage/persistent-storage.md @@ -242,6 +242,60 @@ You can pass flags which will output only the value of the specific information dokku storage:report node-js-app --storage-deploy-mounts ``` +In addition to the aggregated `Storage build/deploy/run mounts:` lines, the report emits one flat dotted key per attachment field, indexed from `1`. The key shape is `--storage-attachment..` for each of `entry-name`, `host-path`, `container-path`, `phases`, `process-type`, `subpath`, `readonly`, `volume-options`, and `volume-chown`. Fields render as empty strings when unset, and attachments are ordered by lex-sort of the index (so `10` sorts before `2`): + +```shell +dokku storage:create node-js-data +dokku storage:mount node-js-app node-js-data --container-dir /app/storage --volume-options Z --volume-chown herokuish --volume-subpath uploads +dokku storage:report node-js-app +``` + +``` +=====> node-js-app storage information + Storage attachment 1 container path: /app/storage + Storage attachment 1 entry name: node-js-data + Storage attachment 1 host path: /var/lib/dokku/data/storage/node-js-data + Storage attachment 1 phases: deploy,run + Storage attachment 1 process type: _default_ + Storage attachment 1 readonly: false + Storage attachment 1 subpath: uploads + Storage attachment 1 volume chown: herokuish + Storage attachment 1 volume options: Z + Storage build mounts: + Storage deploy mounts: -v /var/lib/dokku/data/storage/node-js-data:/app/storage:Z + Storage run mounts: -v /var/lib/dokku/data/storage/node-js-data:/app/storage:Z +``` + +The same keys are exposed in JSON output, both in the stripped (`attachment.1.volume-options`) and legacy (`storage-attachment.1.volume-options`) forms: + +```shell +dokku storage:report node-js-app --format json | jq '. | with_entries(select(.key | startswith("attachment.")))' +``` + +```json +{ + "attachment.1.container-path": "/app/storage", + "attachment.1.entry-name": "node-js-data", + "attachment.1.host-path": "/var/lib/dokku/data/storage/node-js-data", + "attachment.1.phases": "deploy,run", + "attachment.1.process-type": "_default_", + "attachment.1.readonly": "false", + "attachment.1.subpath": "uploads", + "attachment.1.volume-chown": "herokuish", + "attachment.1.volume-options": "Z" +} +``` + +A single attachment field can be fetched directly via the info-flag form: + +```shell +dokku storage:report node-js-app --storage-attachment.1.volume-options +``` + +``` +Z +``` + ## Use Cases ### Sharing storage across deploys diff --git a/plugins/storage/report.go b/plugins/storage/report.go index 25f45df27..9e4dd3121 100644 --- a/plugins/storage/report.go +++ b/plugins/storage/report.go @@ -1,6 +1,12 @@ package storage -import "github.com/dokku/dokku/plugins/common" +import ( + "fmt" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" +) func ReportSingleApp(appName string, format string, infoFlag string) error { if appName != "--global" { @@ -9,14 +15,26 @@ func ReportSingleApp(appName string, format string, infoFlag string) error { } } - var flags map[string]common.ReportFunc - if appName == "--global" { - flags = map[string]common.ReportFunc{} - } else { - flags = map[string]common.ReportFunc{ - "--storage-build-mounts": reportBuildMounts, - "--storage-deploy-mounts": reportDeployMounts, - "--storage-run-mounts": reportRunMounts, + flags := map[string]common.ReportFunc{} + if appName != "--global" { + flags["--storage-build-mounts"] = reportBuildMounts + flags["--storage-deploy-mounts"] = reportDeployMounts + flags["--storage-run-mounts"] = reportRunMounts + + attachments, err := LoadAttachments(appName) + if err != nil { + common.LogWarn(fmt.Sprintf("Unable to load storage attachments for %q: %s", appName, err)) + attachments = nil + } + index := 0 + for _, attachment := range attachments { + entry, err := LoadEntry(attachment.EntryName) + if err != nil { + common.LogWarn(fmt.Sprintf("Skipping attachment on %q: missing entry %q (%s)", appName, attachment.EntryName, err)) + continue + } + index++ + registerAttachmentFlags(flags, index, attachment, entry) } } @@ -50,3 +68,25 @@ func reportDeployMounts(appName string) string { func reportRunMounts(appName string) string { return GetBindMountsForDisplay(appName, "run") } + +// registerAttachmentFlags adds one report flag per Attachment field for the +// given 1-based index. Host path is resolved from the referenced Entry +// (falling back to entry.Name when the entry has no host path, matching +// ListAppMountEntries). +func registerAttachmentFlags(flags map[string]common.ReportFunc, index int, att *Attachment, entry *Entry) { + prefix := fmt.Sprintf("--storage-attachment.%d.", index) + captured := att + host := entry.HostPath + if host == "" { + host = entry.Name + } + flags[prefix+"entry-name"] = func(string) string { return captured.EntryName } + flags[prefix+"host-path"] = func(string) string { return host } + flags[prefix+"container-path"] = func(string) string { return captured.ContainerPath } + flags[prefix+"phases"] = func(string) string { return strings.Join(captured.Phases, ",") } + flags[prefix+"process-type"] = func(string) string { return captured.ProcessType } + flags[prefix+"subpath"] = func(string) string { return captured.Subpath } + flags[prefix+"readonly"] = func(string) string { return strconv.FormatBool(captured.Readonly) } + flags[prefix+"volume-options"] = func(string) string { return captured.VolumeOptions } + flags[prefix+"volume-chown"] = func(string) string { return captured.VolumeChown } +} diff --git a/tests/unit/storage.bats b/tests/unit/storage.bats index a8bbc02be..662f3694f 100644 --- a/tests/unit/storage.bats +++ b/tests/unit/storage.bats @@ -197,6 +197,10 @@ teardown() { 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 @@ -212,10 +216,144 @@ teardown() { 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:list-entries / storage:destroy" { run /bin/bash -c "dokku storage:create rdmtest-entry" echo "output: $output" @@ -734,4 +872,19 @@ teardown() { 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 }