From cbde55a4a0e930a996dd018c90a6d17525004efa Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 11 Oct 2026 04:13:52 -0400 Subject: [PATCH 1/3] fix: respect DOKKU_LIB_HOST_ROOT for storage bind mounts When Dokku runs in Docker with a shared socket, create/chown still use DOKKU_LIB_ROOT inside the container, but Docker -v flags for storage:exec and deploy must use DOKKU_LIB_HOST_ROOT so the daemon mounts the host data volume instead of the real host's /var/lib/dokku. Closes #9126. --- docs/advanced-usage/persistent-storage.md | 2 + docs/getting-started/install/docker.md | 3 +- .../scheduler-docker-local/storage_exec.go | 2 +- .../storage_exec_test.go | 22 ++++++++++ plugins/storage/storage.go | 21 +++++++++ plugins/storage/storage_test.go | 43 +++++++++++++++++++ plugins/storage/triggers.go | 2 +- plugins/storage/triggers_test.go | 11 +++++ 8 files changed, 103 insertions(+), 3 deletions(-) diff --git a/docs/advanced-usage/persistent-storage.md b/docs/advanced-usage/persistent-storage.md index d7884d758..12acdbca8 100644 --- a/docs/advanced-usage/persistent-storage.md +++ b/docs/advanced-usage/persistent-storage.md @@ -88,6 +88,8 @@ Each entry mirrors the underlying attachment. `readonly` (boolean) and `volume_o A storage directory can be created with the `storage:ensure-directory` command. This command will create a subdirectory in the recommended `/var/lib/dokku/data/storage` path - created during Dokku installation - and prepare it for use with an app. +When Dokku itself runs in a container with a shared Docker socket, default named storage is still created under `DOKKU_LIB_ROOT` (typically `/var/lib/dokku`) inside the Dokku container. Docker `-v` mounts for `storage:exec` and app deploys rewrite that path to `DOKKU_LIB_HOST_ROOT` so the host Docker daemon binds the directory from the host-visible data volume. See the [Docker installation notes](/docs/getting-started/install/docker.md) for setting `DOKKU_LIB_HOST_ROOT`. + ```shell dokku storage:ensure-directory node-js-app ``` diff --git a/docs/getting-started/install/docker.md b/docs/getting-started/install/docker.md index 86f5f3b0c..225612648 100644 --- a/docs/getting-started/install/docker.md +++ b/docs/getting-started/install/docker.md @@ -87,8 +87,9 @@ Dokku is run in the following configuration: - Data within the container is stored on the host within the `/var/lib/dokku` directory. - Image build cache is set to the data dir + `/home/dokku`. - The docker socket is mounted into container. +- `DOKKU_HOST_ROOT` and `DOKKU_LIB_HOST_ROOT` tell Dokku how the host Docker daemon sees those same trees when creating bind mounts (app repos and plugin data such as named storage). -Application repositories, plugin config, as well as plugin data are persisted to disk within the specified host directory for `/var/lib/dokku`. +Application repositories, plugin config, as well as plugin data are persisted to disk within the specified host directory for `/var/lib/dokku`. `DOKKU_LIB_HOST_ROOT` must point at that host path's `var/lib/dokku` subtree so Docker `-v` mounts resolve under the data volume rather than under the host's own `/var/lib/dokku`. Other docker container options can also be used when running Dokku, though the specific outcome will depend upon the specified options. For example, the Dokku container's nginx port can be bound to a specific host ip by specifying `--publish $HOST_IP:8080:80`, where `$HOST_IP` is the IP desired. Please see the [docker container run documentation](https://docs.docker.com/engine/reference/commandline/run/) for further explanation for various docker arguments. diff --git a/plugins/scheduler-docker-local/storage_exec.go b/plugins/scheduler-docker-local/storage_exec.go index 4db397e29..c2d8629dd 100644 --- a/plugins/scheduler-docker-local/storage_exec.go +++ b/plugins/scheduler-docker-local/storage_exec.go @@ -137,7 +137,7 @@ func buildDockerExecArgs(entry *storage.Entry, input StorageExecInput) ([]string args = append(args, "--user", user) } - args = append(args, "-v", fmt.Sprintf("%s:/data", entry.HostPath)) + args = append(args, "-v", fmt.Sprintf("%s:/data", storage.HostPathForDocker(entry.HostPath))) args = append(args, input.Image) args = append(args, cmd...) return args, nil diff --git a/plugins/scheduler-docker-local/storage_exec_test.go b/plugins/scheduler-docker-local/storage_exec_test.go index 110bab0eb..79556de30 100644 --- a/plugins/scheduler-docker-local/storage_exec_test.go +++ b/plugins/scheduler-docker-local/storage_exec_test.go @@ -116,3 +116,25 @@ func TestBuildDockerExecArgsMountsHostPath(t *testing.T) { t.Fatalf("expected -v /srv/demo:/data, got: %s", joined) } } + +func TestBuildDockerExecArgsRewritesLibHostRoot(t *testing.T) { + t.Setenv("DOKKU_LIB_ROOT", "/var/lib/dokku") + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + + entry := &storage.Entry{ + Name: "demo-data", + Scheduler: storage.SchedulerDockerLocal, + HostPath: "/var/lib/dokku/data/storage/demo-data", + } + args, err := buildDockerExecArgs(entry, StorageExecInput{ + Image: "alpine:3", + Command: []string{"ls"}, + }) + if err != nil { + t.Fatalf("err: %v", err) + } + joined := strings.Join(args, " ") + if !strings.Contains(joined, "-v /host/var/lib/dokku/data/storage/demo-data:/data") { + t.Fatalf("expected rewritten -v host path, got: %s", joined) + } +} diff --git a/plugins/storage/storage.go b/plugins/storage/storage.go index 18fc19447..987f583f0 100644 --- a/plugins/storage/storage.go +++ b/plugins/storage/storage.go @@ -3,6 +3,7 @@ package storage import ( "errors" "fmt" + "os" "path/filepath" "regexp" "strings" @@ -190,6 +191,26 @@ func GetStorageDirectory() string { return fmt.Sprintf("%s/data/storage", dokkuLibRoot) } +// HostPathForDocker rewrites a path under DOKKU_LIB_ROOT to the +// DOKKU_LIB_HOST_ROOT view used by the host Docker daemon. Named +// volumes and paths outside DOKKU_LIB_ROOT are returned unchanged. +// +// When Dokku runs in a container with a shared Docker socket, filesystem +// operations must use DOKKU_LIB_ROOT (visible inside the Dokku container) +// while Docker -v host paths must use DOKKU_LIB_HOST_ROOT (visible to the +// daemon on the real host). +func HostPathForDocker(path string) string { + libRoot := common.GetenvWithDefault("DOKKU_LIB_ROOT", "/var/lib/dokku") + libHost := os.Getenv("DOKKU_LIB_HOST_ROOT") + if libHost == "" || libHost == libRoot { + return path + } + if path == libRoot || strings.HasPrefix(path, libRoot+"/") { + return libHost + strings.TrimPrefix(path, libRoot) + } + return path +} + // storageDirScriptNames lists the sudo helpers shipped in the plugin's bin // directory. Each one takes a storage entry basename and builds the path // under $DOKKU_LIB_ROOT/data/storage itself, so no caller can point them diff --git a/plugins/storage/storage_test.go b/plugins/storage/storage_test.go index 871bdf53a..854040016 100644 --- a/plugins/storage/storage_test.go +++ b/plugins/storage/storage_test.go @@ -107,3 +107,46 @@ func TestGetStorageDirectory(t *testing.T) { dir := GetStorageDirectory() Expect(dir).To(ContainSubstring("data/storage")) } + +func TestHostPathForDocker(t *testing.T) { + RegisterTestingT(t) + + t.Setenv("DOKKU_LIB_ROOT", "/var/lib/dokku") + + t.Run("unset host root leaves path unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "") + Expect(HostPathForDocker("/var/lib/dokku/data/storage/foo")).To(Equal("/var/lib/dokku/data/storage/foo")) + }) + + t.Run("equal host root leaves path unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/var/lib/dokku") + Expect(HostPathForDocker("/var/lib/dokku/data/storage/foo")).To(Equal("/var/lib/dokku/data/storage/foo")) + }) + + t.Run("rewrites paths under lib root", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("/var/lib/dokku/data/storage/foo")).To(Equal("/host/var/lib/dokku/data/storage/foo")) + Expect(HostPathForDocker("/var/lib/dokku")).To(Equal("/host/var/lib/dokku")) + }) + + t.Run("leaves named volumes unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("myvolume")).To(Equal("myvolume")) + }) + + t.Run("leaves custom absolute paths unchanged", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("/srv/demo")).To(Equal("/srv/demo")) + }) + + t.Run("does not false-match similar prefixes", func(t *testing.T) { + RegisterTestingT(t) + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + Expect(HostPathForDocker("/var/lib/dokku-extra/data")).To(Equal("/var/lib/dokku-extra/data")) + }) +} diff --git a/plugins/storage/triggers.go b/plugins/storage/triggers.go index 144986331..d35a767aa 100644 --- a/plugins/storage/triggers.go +++ b/plugins/storage/triggers.go @@ -308,7 +308,7 @@ var appSchedulerFor = common.GetAppScheduler // buildDockerVFlag formats the Docker -v argument for a docker-local // attachment. func buildDockerVFlag(entry *Entry, attachment *Attachment) string { - host := entry.HostPath + host := HostPathForDocker(entry.HostPath) container := attachment.ContainerPath if host == "" || container == "" { return "" diff --git a/plugins/storage/triggers_test.go b/plugins/storage/triggers_test.go index 29b4b311e..f8913be75 100644 --- a/plugins/storage/triggers_test.go +++ b/plugins/storage/triggers_test.go @@ -61,6 +61,17 @@ func TestBuildDockerVFlagVolumeOptions(t *testing.T) { Expect(roOpts).To(Equal("-v /host:/container:ro,noexec,nosuid")) } +func TestBuildDockerVFlagRewritesLibHostRoot(t *testing.T) { + RegisterTestingT(t) + + t.Setenv("DOKKU_LIB_ROOT", "/var/lib/dokku") + t.Setenv("DOKKU_LIB_HOST_ROOT", "/host/var/lib/dokku") + + entry := &Entry{HostPath: "/var/lib/dokku/data/storage/demo"} + flag := buildDockerVFlag(entry, &Attachment{ContainerPath: "/app/data"}) + Expect(flag).To(Equal("-v /host/var/lib/dokku/data/storage/demo:/app/data")) +} + func TestTriggerDockerArgsRejectsSchedulerMismatch(t *testing.T) { RegisterTestingT(t) root := withTempLibRoot(t) From f768eb6d0773f039d25e76d79a94a3b1f64d3f09 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Sun, 11 Oct 2026 04:19:47 -0400 Subject: [PATCH 2/3] test: cover DOKKU_LIB_HOST_ROOT storage mounts in bats Add a Dokku-in-Docker bats suite that writes under DOKKU_LIB_HOST_ROOT and checks storage:exec, set those env vars in the docker CI container, and add unit bats for the bare-metal (unset) and rewritten docker-args paths. --- tests.mk | 6 +- tests/ci/setup.sh | 5 ++ tests/docker/storage.bats | 49 ++++++++++++++++ tests/unit/storage-5.bats | 117 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 tests/docker/storage.bats create mode 100644 tests/unit/storage-5.bats diff --git a/tests.mk b/tests.mk index 08707ddd3..576f4fbd7 100644 --- a/tests.mk +++ b/tests.mk @@ -336,7 +336,11 @@ tests-ci-retry-failed: tar xzf /tmp/bats-retry.tgz -C /usr/local/bin bats-retry --execute test-results/bats -test-ci-docker: setup-docker-deploy-tests deploy-test-checks-root deploy-test-config deploy-test-multi deploy-test-go-fail-predeploy deploy-test-go-fail-postdeploy test-ci-binaries +deploy-test-storage-lib-host-root: + @echo deploy-test-storage-lib-host-root + cd tests/docker && bats storage.bats + +test-ci-docker: setup-docker-deploy-tests deploy-test-checks-root deploy-test-config deploy-test-multi deploy-test-go-fail-predeploy deploy-test-go-fail-postdeploy deploy-test-storage-lib-host-root test-ci-binaries generate-ssl-tars: generate-ssl-tar generate-ssl-sans-tar generate-ssl-wildcard-tar diff --git a/tests/ci/setup.sh b/tests/ci/setup.sh index 1cb1d4e80..e07edab5d 100755 --- a/tests/ci/setup.sh +++ b/tests/ci/setup.sh @@ -123,8 +123,13 @@ build_dokku_docker_image() { run_dokku_container() { echo "=====> run_dokku_container on CIRCLE_NODE_INDEX: $CIRCLE_NODE_INDEX" echo "redirect: https://github.com/dokku/dokku-redirect.git" | sudo tee /var/lib/dokku/plugin-list + # DOKKU_HOST_ROOT / DOKKU_LIB_HOST_ROOT match the install docs: the host + # mounts /var/lib/dokku at /mnt/dokku, and in-container /var/lib/dokku/{home,data} + # are symlinks into that volume. docker run -d \ --env DOKKU_HOSTNAME=dokku.me \ + --env DOKKU_HOST_ROOT=/var/lib/dokku/home/dokku \ + --env DOKKU_LIB_HOST_ROOT=/var/lib/dokku/var/lib/dokku \ --name dokku \ --publish 3022:22 \ --publish 80:80 \ diff --git a/tests/docker/storage.bats b/tests/docker/storage.bats new file mode 100644 index 000000000..bad567729 --- /dev/null +++ b/tests/docker/storage.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats + +# Dokku-in-Docker coverage for DOKKU_LIB_HOST_ROOT. These tests talk to the +# `dokku` container started by tests/ci/setup.sh docker and read/write the +# host path that volume backs (/var/lib/dokku -> /mnt/dokku). + +setup_file() { + if ! docker inspect dokku >/dev/null 2>&1; then + echo "dokku container is not running; skip docker storage tests" >&2 + return 1 + fi + + DOKKU_LIB_HOST_ROOT="$(docker exec dokku bash -c 'echo -n "$DOKKU_LIB_HOST_ROOT"')" + if [[ -z "$DOKKU_LIB_HOST_ROOT" ]]; then + echo "DOKKU_LIB_HOST_ROOT is unset inside the dokku container" >&2 + return 1 + fi + export DOKKU_LIB_HOST_ROOT +} + +setup() { + ENTRY_NAME="rdmtest-docker-hostroot-$$" + export ENTRY_NAME +} + +teardown() { + docker exec dokku dokku storage:destroy "$ENTRY_NAME" --force >/dev/null 2>&1 || true + sudo rm -rf "${DOKKU_LIB_HOST_ROOT}/data/storage/${ENTRY_NAME}" +} + +@test "(docker) storage:exec sees a file written under DOKKU_LIB_HOST_ROOT" { + run docker exec dokku dokku storage:create "$ENTRY_NAME" --chown root + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + + # storage:create mkdirs under DOKKU_LIB_ROOT inside the container, which is + # the same tree as DOKKU_LIB_HOST_ROOT on the host via the /mnt/dokku volume. + HOST_STORAGE_DIR="${DOKKU_LIB_HOST_ROOT}/data/storage/${ENTRY_NAME}" + [[ -d "$HOST_STORAGE_DIR" ]] + + echo "Hello World!" | sudo tee "${HOST_STORAGE_DIR}/welcome.txt" >/dev/null + + run docker exec dokku dokku storage:exec "$ENTRY_NAME" -- cat /data/welcome.txt + echo "output: $output" + echo "status: $status" + [[ "$status" -eq 0 ]] + [[ "$output" == *"Hello World!"* ]] +} diff --git a/tests/unit/storage-5.bats b/tests/unit/storage-5.bats new file mode 100644 index 000000000..dd9b6f78b --- /dev/null +++ b/tests/unit/storage-5.bats @@ -0,0 +1,117 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + global_setup + create_app + rm -rf "${DOKKU_LIB_ROOT}"/data/storage/rdmtest* +} + +teardown() { + destroy_app + global_teardown +} + +@test "(storage) without DOKKU_LIB_HOST_ROOT docker-args and storage:exec use DOKKU_LIB_ROOT" { + unset DOKKU_LIB_HOST_ROOT + + run /bin/bash -c "dokku storage:create rdmtest-bare" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-bare --container-dir /data" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo '' | dokku plugin:trigger docker-args-process-deploy $TEST_APP herokuish latest web | grep -o -- '-v [^ ]*'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "-v ${DOKKU_LIB_ROOT}/data/storage/rdmtest-bare:/data" + + run /bin/bash -c "docker pull alpine:3 >/dev/null" + assert_success + + run /bin/bash -c "dokku storage:exec rdmtest-bare -- /bin/sh -c 'echo bare-metal > /data/marker && cat /data/marker'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "bare-metal" + + run /bin/bash -c "cat ${DOKKU_LIB_ROOT}/data/storage/rdmtest-bare/marker" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "bare-metal" + + run /bin/bash -c "dokku storage:unmount --all $TEST_APP" + assert_success + run /bin/bash -c "dokku storage:destroy rdmtest-bare --force" + assert_success +} + +@test "(storage) with DOKKU_LIB_HOST_ROOT docker-args rewrites the mount host path" { + HOST_ROOT="$(mktemp -d /tmp/dokku-lib-host.XXXXXX)" + export DOKKU_LIB_HOST_ROOT="$HOST_ROOT" + + run /bin/bash -c "dokku storage:create rdmtest-hostroot" + echo "output: $output" + echo "status: $status" + assert_success + + # Create still lands under DOKKU_LIB_ROOT inside Dokku. + run /bin/bash -c "test -d ${DOKKU_LIB_ROOT}/data/storage/rdmtest-hostroot" + assert_success + + run /bin/bash -c "dokku storage:mount $TEST_APP rdmtest-hostroot --container-dir /data" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "echo '' | dokku plugin:trigger docker-args-process-deploy $TEST_APP herokuish latest web | grep -o -- '-v [^ ]*'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "-v ${HOST_ROOT}/data/storage/rdmtest-hostroot:/data" + + run /bin/bash -c "dokku storage:unmount --all $TEST_APP" + assert_success + run /bin/bash -c "dokku storage:destroy rdmtest-hostroot --force" + assert_success + rm -rf "$HOST_ROOT" +} + +@test "(storage) with DOKKU_LIB_HOST_ROOT storage:exec mounts the host-root path" { + # Simulate Dokku-in-Docker: filesystem ops use DOKKU_LIB_ROOT, but Docker -v + # mounts must resolve under DOKKU_LIB_HOST_ROOT. + HOST_ROOT="$(mktemp -d /tmp/dokku-lib-host.XXXXXX)" + export DOKKU_LIB_HOST_ROOT="$HOST_ROOT" + + run /bin/bash -c "dokku storage:create rdmtest-hostroot-exec --chown root" + echo "output: $output" + echo "status: $status" + assert_success + + mkdir -p "${HOST_ROOT}/data/storage/rdmtest-hostroot-exec" + echo "Hello World!" >"${HOST_ROOT}/data/storage/rdmtest-hostroot-exec/welcome.txt" + + run /bin/bash -c "docker pull alpine:3 >/dev/null" + assert_success + + run /bin/bash -c "dokku storage:exec rdmtest-hostroot-exec -- cat /data/welcome.txt" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Hello World!" + + # The in-container create path must not have received the host-written file. + run /bin/bash -c "test ! -f ${DOKKU_LIB_ROOT}/data/storage/rdmtest-hostroot-exec/welcome.txt" + assert_success + + run /bin/bash -c "dokku storage:destroy rdmtest-hostroot-exec --force" + assert_success + rm -rf "$HOST_ROOT" +} From 3dafdcb3f9227584cb65ec01bf56e67bd75ef3a0 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 14 Oct 2026 17:27:22 -0400 Subject: [PATCH 3/3] fix: detect storage:exec TTY with isatty, not ModeCharDevice /dev/null is a character device but not a terminal. Treating it as a TTY made docker run -it fail under docker exec without -t, which broke the Dokku-in-Docker storage bats job in CI. --- plugins/storage/commands_entries.go | 15 ++++---- plugins/storage/commands_entries_test.go | 45 ++++++++++++++++++++++++ plugins/storage/go.mod | 2 +- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/plugins/storage/commands_entries.go b/plugins/storage/commands_entries.go index fd8624ccc..e35a6afcd 100644 --- a/plugins/storage/commands_entries.go +++ b/plugins/storage/commands_entries.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/dokku/dokku/plugins/common" + "github.com/mattn/go-isatty" ) // CommandCreateInput captures the flags accepted by storage:create. @@ -422,14 +423,16 @@ func CommandExec(input CommandExecInput) error { // stdinModes inspects os.Stdin to decide whether docker / the k8s exec // SDK should request an interactive session and a TTY. +// +// TTY detection uses isatty rather than ModeCharDevice: /dev/null is a +// character device but not a terminal, and treating it as a TTY makes +// docker run -it fail with "cannot attach stdin to a TTY-enabled +// container because stdin is not a terminal" (e.g. docker exec without +// -t, as in CI). func stdinModes() (interactive bool, tty bool) { + tty = isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) fi, err := os.Stdin.Stat() - if err != nil || fi == nil { - return false, false - } - mode := fi.Mode() - tty = mode&os.ModeCharDevice != 0 - hasStdinData := mode&os.ModeNamedPipe != 0 + hasStdinData := err == nil && fi != nil && fi.Mode()&os.ModeNamedPipe != 0 interactive = tty || hasStdinData return interactive, tty } diff --git a/plugins/storage/commands_entries_test.go b/plugins/storage/commands_entries_test.go index 41139a880..58a842970 100644 --- a/plugins/storage/commands_entries_test.go +++ b/plugins/storage/commands_entries_test.go @@ -1,6 +1,7 @@ package storage import ( + "os" "path/filepath" "testing" @@ -218,3 +219,47 @@ func TestCommandSetPersistsAndValidates(t *testing.T) { Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("does not exist")) } + +// TestStdinModesDevNullIsNotTTY guards the CI / docker-exec path: stdin is +// often /dev/null, which is a character device but not a terminal. ModeCharDevice +// alone would request docker run -it and fail attach. +func TestStdinModesDevNullIsNotTTY(t *testing.T) { + devNull, err := os.Open(os.DevNull) + if err != nil { + t.Fatal(err) + } + defer devNull.Close() + + old := os.Stdin + os.Stdin = devNull + t.Cleanup(func() { os.Stdin = old }) + + interactive, tty := stdinModes() + if tty { + t.Fatalf("expected tty=false for /dev/null, got tty=true interactive=%v", interactive) + } + if interactive { + t.Fatalf("expected interactive=false for /dev/null, got interactive=true") + } +} + +func TestStdinModesNamedPipeIsInteractiveWithoutTTY(t *testing.T) { + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + old := os.Stdin + os.Stdin = r + t.Cleanup(func() { os.Stdin = old }) + + interactive, tty := stdinModes() + if tty { + t.Fatalf("expected tty=false for a pipe, got tty=true") + } + if !interactive { + t.Fatalf("expected interactive=true for a named pipe") + } +} diff --git a/plugins/storage/go.mod b/plugins/storage/go.mod index 1c93444bf..66410f03a 100644 --- a/plugins/storage/go.mod +++ b/plugins/storage/go.mod @@ -5,6 +5,7 @@ go 1.26.2 require ( github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 github.com/dokku/dokku/plugins/docker-options v0.0.0-00010101000000-000000000000 + github.com/mattn/go-isatty v0.0.24 github.com/onsi/gomega v1.44.0 github.com/spf13/pflag v1.0.10 ) @@ -17,7 +18,6 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/kr/fs v0.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.24 // indirect github.com/melbahja/goph v1.5.2 // indirect github.com/otiai10/copy v1.14.1 // indirect github.com/otiai10/mint v1.6.3 // indirect