Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions cmd/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ type runOptions struct {
Detach bool
Remove bool
noTty bool
tty bool
interactive bool
user string
workdir string
Expand Down Expand Up @@ -155,6 +154,10 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
buildOpts := buildOptions{
ProjectOptions: p,
}
// We remove the attribute from the option struct and use a dedicated var, to limit confusion and avoid anyone to use options.tty.
// The tty flag is here for convenience and let user do "docker compose run -it" the same way as they use the "docker run" command.
var ttyFlag bool

cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service",
Expand All @@ -178,9 +181,13 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
} else {
options.noTty = !options.tty
options.noTty = !ttyFlag
}
} else if !cmd.Flags().Changed("no-TTY") && !cmd.Flags().Changed("interactive") && !dockerCli.In().IsTerminal() {
// Check if the command was piped or not, if so, force noTty to tru
options.noTty = true
}

if options.quiet {
progress.Mode = progress.ModeQuiet
devnull, err := os.Open(os.DevNull)
Expand Down Expand Up @@ -238,7 +245,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *
flags.BoolVar(&options.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file")

cmd.Flags().BoolVarP(&options.interactive, "interactive", "i", true, "Keep STDIN open even if not attached")
cmd.Flags().BoolVarP(&options.tty, "tty", "t", true, "Allocate a pseudo-TTY")
cmd.Flags().BoolVarP(&ttyFlag, "tty", "t", true, "Allocate a pseudo-TTY")
cmd.Flags().MarkHidden("tty") //nolint:errcheck

flags.SetNormalizeFunc(normalizeRunFlags)
Expand Down
56 changes: 56 additions & 0 deletions pkg/e2e/compose_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,60 @@ func TestLocalComposeRun(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/run-test/compose.yaml", "run", "build", "echo", "hello world")
res.Assert(t, icmd.Expected{Out: "hello world"})
})

t.Run("compose run with piped input detection", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that piped input is properly detected and TTY is automatically disabled
// This tests the logic added in run.go that checks dockerCli.In().IsTerminal()
cmd := c.NewCmd("sh", "-c", "echo 'piped-content' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm piped-test")
res := icmd.RunCmd(cmd)

res.Assert(t, icmd.Expected{Out: "piped-content"})
res.Assert(t, icmd.Success)
})

t.Run("compose run piped input should not allocate TTY", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that when stdin is piped, the container correctly detects no TTY
// This verifies that the automatic noTty=true setting works correctly
cmd := c.NewCmd("sh", "-c", "echo '' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm tty-test")
res := icmd.RunCmd(cmd)

res.Assert(t, icmd.Expected{Out: "No TTY detected"})
res.Assert(t, icmd.Success)
})

t.Run("compose run piped input with explicit --tty should fail", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that explicitly requesting TTY with piped input fails with proper error message
// This should trigger the "input device is not a TTY" error
cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm --tty piped-test")
res := icmd.RunCmd(cmd)

res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "the input device is not a TTY",
})
})

t.Run("compose run piped input with --no-TTY=false should fail", func(t *testing.T) {
if composeStandaloneMode {
t.Skip("Skipping test compose with piped input detection in standalone mode")
}
// Test that explicitly disabling --no-TTY (i.e., requesting TTY) with piped input fails
// This should also trigger the "input device is not a TTY" error
cmd := c.NewCmd("sh", "-c", "echo 'test' | docker compose -f ./fixtures/run-test/piped-test.yaml run --rm --no-TTY=false piped-test")
res := icmd.RunCmd(cmd)

res.Assert(t, icmd.Expected{
ExitCode: 1,
Err: "the input device is not a TTY",
})
})
}
9 changes: 9 additions & 0 deletions pkg/e2e/fixtures/run-test/piped-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
piped-test:
image: alpine
command: cat
# Service that will receive piped input and echo it back
tty-test:
image: alpine
command: sh -c "if [ -t 0 ]; then echo 'TTY detected'; else echo 'No TTY detected'; fi"
# Service to test TTY detection
Loading