feat: prompt for confirmation on storage:destroy
`storage:destroy` now prompts for confirmation before removing a named storage entry, matching the behavior of other destructive commands such as `apps:destroy` and `network:destroy`. The prompt can be skipped with the `--force` flag or the global `dokku --force` flag for non-interactive callers.
This commit is contained in:
@@ -7,7 +7,7 @@ The preferred method to attach persistent storage to a Dokku-managed container i
|
|||||||
|
|
||||||
```
|
```
|
||||||
storage:create <name> [<path>] [flags] # Register a named storage entry
|
storage:create <name> [<path>] [flags] # Register a named storage entry
|
||||||
storage:destroy <name> # Remove a named storage entry (must be unmounted from every app first)
|
storage:destroy <name> [--force] # Remove a named storage entry (must be unmounted from every app first)
|
||||||
storage:ensure-directory [--chown option] <directory> # [DEPRECATED] use storage:create instead
|
storage:ensure-directory [--chown option] <directory> # [DEPRECATED] use storage:create instead
|
||||||
storage:exec <name> [-- <cmd>...] # Run a command (or shell) in a temporary container that mounts the entry
|
storage:exec <name> [-- <cmd>...] # Run a command (or shell) in a temporary container that mounts the entry
|
||||||
storage:info <name> [--format text|json] # Show details for one storage entry
|
storage:info <name> [--format text|json] # Show details for one storage entry
|
||||||
@@ -140,6 +140,36 @@ Once persistent storage is unmounted, the app requires a restart. See the [proce
|
|||||||
dokku ps:restart app-name
|
dokku ps:restart app-name
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Destroying storage entries
|
||||||
|
|
||||||
|
A named storage entry can be removed with the `storage:destroy` command. The entry must first be unmounted from every app that mounts it.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dokku storage:destroy rdmtest-entry
|
||||||
|
```
|
||||||
|
|
||||||
|
As the command is destructive - removing the registry entry and, depending on the scheduler and reclaim policy, the underlying volume - it will default to asking for confirmation before executing the removal.
|
||||||
|
|
||||||
|
```
|
||||||
|
! WARNING: Potentially Destructive Action
|
||||||
|
! This command will destroy storage entry rdmtest-entry.
|
||||||
|
! To proceed, type "rdmtest-entry"
|
||||||
|
> rdmtest-entry
|
||||||
|
-----> Storage entry rdmtest-entry destroyed
|
||||||
|
```
|
||||||
|
|
||||||
|
The confirmation may be avoided by providing the `--force` flag, which is useful for non-interactive or automated callers:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dokku storage:destroy rdmtest-entry --force
|
||||||
|
```
|
||||||
|
|
||||||
|
The global `--force` flag is also supported:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dokku --force storage:destroy rdmtest-entry
|
||||||
|
```
|
||||||
|
|
||||||
### Displaying storage reports for an app
|
### Displaying storage reports for an app
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
@@ -95,8 +95,10 @@ func CommandCreate(input CommandCreateInput) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CommandDestroy removes a registered storage entry. Refuses to remove
|
// CommandDestroy removes a registered storage entry. Refuses to remove
|
||||||
// an entry that any app still has attached.
|
// an entry that any app still has attached. Prompts for confirmation
|
||||||
func CommandDestroy(name string) error {
|
// unless force is set (or the global --force flag exported
|
||||||
|
// DOKKU_APPS_FORCE_DELETE).
|
||||||
|
func CommandDestroy(name string, force bool) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return errors.New("storage entry name is required")
|
return errors.New("storage entry name is required")
|
||||||
}
|
}
|
||||||
@@ -112,6 +114,15 @@ func CommandDestroy(name string) error {
|
|||||||
return fmt.Errorf("storage entry %q is still mounted by app(s): %s", name, strings.Join(using, ", "))
|
return fmt.Errorf("storage entry %q is still mounted by app(s): %s", name, strings.Join(using, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("DOKKU_APPS_FORCE_DELETE") == "1" {
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
if !force {
|
||||||
|
if err := common.AskForDestructiveConfirmation(name, "storage entry"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entry, err := LoadEntry(name)
|
entry, err := LoadEntry(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Additional commands:`
|
|||||||
|
|
||||||
helpContent = `
|
helpContent = `
|
||||||
storage:create <name> [<path>] [flags], Register a named storage entry
|
storage:create <name> [<path>] [flags], Register a named storage entry
|
||||||
storage:destroy <name>, Remove a named storage entry (must be unmounted from every app first)
|
storage:destroy <name> [--force], Remove a named storage entry (must be unmounted from every app first)
|
||||||
storage:ensure-directory [--chown option] <directory>, [DEPRECATED] use storage:create instead
|
storage:ensure-directory [--chown option] <directory>, [DEPRECATED] use storage:create instead
|
||||||
storage:exec <name> [-- <cmd>...], Run a command (or shell) in a temporary container that mounts the entry
|
storage:exec <name> [-- <cmd>...], Run a command (or shell) in a temporary container that mounts the entry
|
||||||
storage:info <name> [--format text|json], Show details for one storage entry
|
storage:info <name> [--format text|json], Show details for one storage entry
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ func main() {
|
|||||||
})
|
})
|
||||||
case "destroy":
|
case "destroy":
|
||||||
args := flag.NewFlagSet("storage:destroy", flag.ExitOnError)
|
args := flag.NewFlagSet("storage:destroy", flag.ExitOnError)
|
||||||
|
force := args.Bool("force", false, "--force: force destroy without confirmation")
|
||||||
args.Parse(os.Args[2:])
|
args.Parse(os.Args[2:])
|
||||||
err = storage.CommandDestroy(args.Arg(0))
|
err = storage.CommandDestroy(args.Arg(0), *force)
|
||||||
case "ensure-directory":
|
case "ensure-directory":
|
||||||
args := flag.NewFlagSet("storage:ensure-directory", flag.ExitOnError)
|
args := flag.NewFlagSet("storage:ensure-directory", flag.ExitOnError)
|
||||||
chown := args.String("chown", "herokuish", "--chown: chown option (herokuish, heroku, paketo, root, false)")
|
chown := args.String("chown", "herokuish", "--chown: chown option (herokuish, heroku, paketo, root, false)")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Additional commands:`
|
|||||||
|
|
||||||
helpContent = `
|
helpContent = `
|
||||||
storage:create <name> [<path>] [flags], Register a named storage entry
|
storage:create <name> [<path>] [flags], Register a named storage entry
|
||||||
storage:destroy <name>, Remove a named storage entry (must be unmounted from every app first)
|
storage:destroy <name> [--force], Remove a named storage entry (must be unmounted from every app first)
|
||||||
storage:ensure-directory [--chown option] <directory>, [DEPRECATED] use storage:create instead
|
storage:ensure-directory [--chown option] <directory>, [DEPRECATED] use storage:create instead
|
||||||
storage:exec <name> [-- <cmd>...], Run a command (or shell) in a temporary container that mounts the entry
|
storage:exec <name> [-- <cmd>...], Run a command (or shell) in a temporary container that mounts the entry
|
||||||
storage:info <name> [--format text|json], Show details for one storage entry
|
storage:info <name> [--format text|json], Show details for one storage entry
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ teardown() {
|
|||||||
assert_success
|
assert_success
|
||||||
assert_output "docker-local"
|
assert_output "docker-local"
|
||||||
|
|
||||||
run /bin/bash -c "dokku storage:destroy rdmtest-entry"
|
run /bin/bash -c "dokku storage:destroy rdmtest-entry --force"
|
||||||
echo "output: $output"
|
echo "output: $output"
|
||||||
echo "status: $status"
|
echo "status: $status"
|
||||||
assert_success
|
assert_success
|
||||||
@@ -307,9 +307,9 @@ teardown() {
|
|||||||
assert_success
|
assert_success
|
||||||
assert_output "0"
|
assert_output "0"
|
||||||
|
|
||||||
run /bin/bash -c "dokku storage:destroy rdmtest-data"
|
run /bin/bash -c "dokku storage:destroy rdmtest-data --force"
|
||||||
assert_success
|
assert_success
|
||||||
run /bin/bash -c "dokku storage:destroy rdmtest-cache"
|
run /bin/bash -c "dokku storage:destroy rdmtest-cache --force"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,10 +327,64 @@ teardown() {
|
|||||||
|
|
||||||
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-busy"
|
run /bin/bash -c "dokku storage:unmount $TEST_APP rdmtest-busy"
|
||||||
assert_success
|
assert_success
|
||||||
run /bin/bash -c "dokku storage:destroy rdmtest-busy"
|
run /bin/bash -c "dokku storage:destroy rdmtest-busy --force"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "(storage:destroy) requires confirmation without --force" {
|
||||||
|
run /bin/bash -c "dokku storage:create rdmtest-confirm"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# No --force and no matching stdin: aborts, entry remains.
|
||||||
|
run /bin/bash -c "dokku storage:destroy rdmtest-confirm < /dev/null"
|
||||||
|
echo "output: $output"
|
||||||
|
echo "status: $status"
|
||||||
|
assert_failure
|
||||||
|
|
||||||
|
run /bin/bash -c "dokku storage:list-entries --format json | jq -r '.[].name' | grep '^rdmtest-confirm$'"
|
||||||
|
echo "output: $output"
|
||||||
|
echo "status: $status"
|
||||||
|
assert_success
|
||||||
|
assert_output "rdmtest-confirm"
|
||||||
|
|
||||||
|
# Matching confirmation via stdin: succeeds and removes the entry.
|
||||||
|
run /bin/bash -c "echo rdmtest-confirm | dokku storage:destroy rdmtest-confirm"
|
||||||
|
echo "output: $output"
|
||||||
|
echo "status: $status"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run /bin/bash -c "dokku storage:list-entries --format json | jq -r '.[].name' | grep '^rdmtest-confirm$' || true"
|
||||||
|
echo "output: $output"
|
||||||
|
echo "status: $status"
|
||||||
|
assert_output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "(storage:destroy) --force skips confirmation" {
|
||||||
|
run /bin/bash -c "dokku storage:create rdmtest-force"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run /bin/bash -c "dokku storage:destroy rdmtest-force --force < /dev/null"
|
||||||
|
echo "output: $output"
|
||||||
|
echo "status: $status"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run /bin/bash -c "dokku storage:list-entries --format json | jq -r '.[].name' | grep '^rdmtest-force$' || true"
|
||||||
|
assert_output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "(storage:destroy) global --force skips confirmation" {
|
||||||
|
run /bin/bash -c "dokku storage:create rdmtest-gforce"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run /bin/bash -c "dokku --force storage:destroy rdmtest-gforce < /dev/null"
|
||||||
|
echo "output: $output"
|
||||||
|
echo "status: $status"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run /bin/bash -c "dokku storage:list-entries --format json | jq -r '.[].name' | grep '^rdmtest-gforce$' || true"
|
||||||
|
assert_output ""
|
||||||
|
}
|
||||||
|
|
||||||
@test "(storage) storage:exec runs a non-interactive command and propagates exit code" {
|
@test "(storage) storage:exec runs a non-interactive command and propagates exit code" {
|
||||||
run /bin/bash -c "dokku storage:create rdmtest-exec"
|
run /bin/bash -c "dokku storage:create rdmtest-exec"
|
||||||
assert_success
|
assert_success
|
||||||
@@ -350,7 +404,7 @@ teardown() {
|
|||||||
echo "status: $status"
|
echo "status: $status"
|
||||||
assert_equal "$status" 42
|
assert_equal "$status" 42
|
||||||
|
|
||||||
run /bin/bash -c "dokku storage:destroy rdmtest-exec"
|
run /bin/bash -c "dokku storage:destroy rdmtest-exec --force"
|
||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +487,7 @@ teardown() {
|
|||||||
if [[ -n "$legacy_name" ]]; then
|
if [[ -n "$legacy_name" ]]; then
|
||||||
run /bin/bash -c "dokku storage:unmount $TEST_APP $legacy_name --container-dir /legacy"
|
run /bin/bash -c "dokku storage:unmount $TEST_APP $legacy_name --container-dir /legacy"
|
||||||
assert_success
|
assert_success
|
||||||
run /bin/bash -c "dokku storage:destroy $legacy_name"
|
run /bin/bash -c "dokku storage:destroy $legacy_name --force"
|
||||||
assert_success
|
assert_success
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user