diff --git a/cmd/podman/containers/run.go b/cmd/podman/containers/run.go index a175f9c29f..fbee95d354 100644 --- a/cmd/podman/containers/run.go +++ b/cmd/podman/containers/run.go @@ -48,12 +48,8 @@ var ( ) var ( - runOpts = entities.ContainerRunOptions{ - OutputStream: os.Stdout, - InputStream: os.Stdin, - ErrorStream: os.Stderr, - } - runRmi bool + runOpts entities.ContainerRunOptions + runRmi bool ) func runFlags(cmd *cobra.Command) { @@ -154,6 +150,11 @@ func run(cmd *cobra.Command, args []string) error { } } + // First set the default streams before they get modified by any flags. + runOpts.OutputStream = os.Stdout + runOpts.InputStream = os.Stdin + runOpts.ErrorStream = os.Stderr + // If -i is not set, clear stdin if !cliVals.Interactive { runOpts.InputStream = nil diff --git a/cmd/podman/root.go b/cmd/podman/root.go index c94d5f9517..3b8b2352b3 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -79,17 +79,24 @@ var ( useSyslog bool requireCleanup = true - noOut = false + + // Defaults for capturing/redirecting the command output since (the) cobra is + // global-hungry and doesn't allow you to attach anything that allows us to + // transform the noStdout BoolVar to a string that we can assign to useStdout. + noStdout = false + useStdout = "" ) func init() { - // Hooks are called before PersistentPreRunE() + // Hooks are called before PersistentPreRunE(). These hooks affect global + // state and are executed after processing the command-line, but before + // actually running the command. cobra.OnInitialize( + stdOutHook, // Caution, this hook redirects stdout and output from any following hooks may be affected. loggingHook, syslogHook, earlyInitHook, configHook, - noOutHook, ) rootFlags(rootCmd, registry.PodmanConfig()) @@ -376,10 +383,23 @@ func loggingHook() { } } -func noOutHook() { - if noOut { - null, _ := os.Open(os.DevNull) - os.Stdout = null +// used for capturing podman's formatted output to some file as per the -out and -noout flags. +func stdOutHook() { + // if noStdOut was specified, then assign /dev/null as the standard file for output. + if noStdout { + useStdout = os.DevNull + } + // if we were given a filename for output, then open that and use it. we end up leaking + // the file since it's intended to be in scope as long as our process is running. + if useStdout != "" { + if fd, err := os.OpenFile(useStdout, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm); err == nil { + os.Stdout = fd + + // if we couldn't open the file for write, then just bail with an error. + } else { + fmt.Fprintf(os.Stderr, "unable to open file for standard output: %s\n", err.Error()) + os.Exit(1) + } } } @@ -415,7 +435,14 @@ func rootFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) { lFlags.StringVar(&podmanConfig.Identity, identityFlagName, ident, "path to SSH identity file, (CONTAINER_SSHKEY)") _ = cmd.RegisterFlagCompletionFunc(identityFlagName, completion.AutocompleteDefault) - lFlags.BoolVar(&noOut, "noout", false, "do not output to stdout") + // Flags that control or influence any kind of output. + outFlagName := "out" + lFlags.StringVar(&useStdout, outFlagName, "", "Send output (stdout) from podman to a file") + _ = cmd.RegisterFlagCompletionFunc(outFlagName, completion.AutocompleteDefault) + + lFlags.BoolVar(&noStdout, "noout", false, "do not output to stdout") + _ = lFlags.MarkHidden("noout") // Superseded by --out + lFlags.BoolVarP(&podmanConfig.Remote, "remote", "r", registry.IsRemote(), "Access remote Podman service") pFlags := cmd.PersistentFlags() if registry.IsRemote() { diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 3c85d81030..2bda9fa6c9 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -95,9 +95,8 @@ and "$graphroot/networks" as rootless. For the CNI backend the default is "/etc/cni/net.d" as root and "$HOME/.config/cni/net.d" as rootless. CNI is deprecated from Podman in the future, use netavark. -#### **--noout** - -Redirect stdout to /dev/null. This command prevents all stdout from the Podman command. The **--noout** option is not block stderr or stdout from containers. +#### **--out**=*path* +Redirect the output of podman to the specified path without affecting the container output or its logs. This parameter can be used to capture the output from any of podman's commands directly into a file and enable suppression of podman's output by specifying /dev/null as the path. To explicitly disable the container logging, the **--log-driver** option should be used. #### **--remote**, **-r** When true, access to the Podman service is remote. Defaults to false. diff --git a/test/system/001-basic.bats b/test/system/001-basic.bats index a2f0f57d7b..e0c7d1a5e9 100644 --- a/test/system/001-basic.bats +++ b/test/system/001-basic.bats @@ -239,6 +239,36 @@ run_podman --noout system connection ls is "$output" "" "output should be empty" } +# Tests --noout to ensure that the output fd can be written to. +@test "podman --noout is actually writing to /dev/null" { + skip_if_remote "unshare only works locally" + skip_if_not_rootless "unshare requires rootless" + run_podman --noout unshare ls + is "$output" "" "output should be empty" +} + +@test "podman version --out writes matching version to a json" { + run_podman version + + # copypasta from version check. we're doing this to extract the version. + if expr "${lines[0]}" : "Client: *" >/dev/null; then + lines=("${lines[@]:1}") + fi + + # get the version number so that we have something to compare with. + IFS=: read version_key version_number <<<"${lines[0]}" + is "$version_key" "Version" "Version line" + + # now we can output everything as some json. we can't use PODMAN_TMPDIR since basic_setup + # isn't being used in setup() due to being unable to trust podman-images or podman-rm. + outfile=$(mktemp -p ${BATS_TEST_TMPDIR} veroutXXXXXXXX) + run_podman --out $outfile version -f json + + # extract the version from the file. + run jq -r --arg field "$version_key" '.Client | .[$field]' $outfile + is "$output" ${version_number} "Version matches" +} + @test "podman - shutdown engines" { run_podman --log-level=debug run --rm $IMAGE true is "$output" ".*Shutting down engines.*" diff --git a/test/system/030-run.bats b/test/system/030-run.bats index db73955a41..85ff2b2b71 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -518,6 +518,37 @@ json-file | f is "$output" "" "output should be empty" } +@test "podman --out run should save the container id" { + outfile=${PODMAN_TMPDIR}/out-results + + # first we'll need to run something, write its output to a file, and then read its contents. + run_podman --out $outfile run -d --name test $IMAGE echo hola + is "$output" "" "output should be redirected" + run_podman wait test + + # compare the container id against the one in the file + run_podman container inspect --format '{{.Id}}' test + is "$output" "$(<$outfile)" "container id should match" + + run_podman --out /dev/null rm test + is "$output" "" "output should be empty" +} + +@test "podman --out create should save the container id" { + outfile=${PODMAN_TMPDIR}/out-results + + # first we'll need to run something, write its output to a file, and then read its contents. + run_podman --out $outfile create --name test $IMAGE echo hola + is "$output" "" "output should be redirected" + + # compare the container id against the one in the file + run_podman container inspect --format '{{.Id}}' test + is "$output" "$(<$outfile)" "container id should match" + + run_podman --out /dev/null rm test + is "$output" "" "output should be empty" +} + # Regression test for issue #8082 @test "podman run : look up correct image name" { # Create a 2nd tag for the local image. Force to lower case, and apply it.