podman machine ssh: set correct exit code

Forward the ssh exit code to the podman caller. This is useful for
scripts. Use the same logic as podman unshare.

Fixes #14401

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger
2022-05-30 15:15:57 +02:00
parent a6f8cad545
commit 0e58636c3a
6 changed files with 82 additions and 20 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/pkg/machine"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -89,7 +90,8 @@ func ssh(cmd *cobra.Command, args []string) error {
if err != nil {
return errors.Wrapf(err, "vm %s not found", vmName)
}
return vm.SSH(vmName, sshOpts)
err = vm.SSH(vmName, sshOpts)
return utils.HandleOSExecError(err)
}
func remoteConnectionUsername() (string, error) {

View File

@ -2,10 +2,10 @@ package system
import (
"os"
"os/exec"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/pkg/errors"
@ -60,22 +60,5 @@ func unshare(cmd *cobra.Command, args []string) error {
}
err := registry.ContainerEngine().Unshare(registry.Context(), args, unshareOptions)
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
// the user command inside the unshare env has failed
// we set the exit code, do not return the error to the user
// otherwise "exit status X" will be printed
registry.SetExitCode(exitError.ExitCode())
return nil
}
// cmd.Run() can return fs.ErrNotExist, fs.ErrPermission or exec.ErrNotFound
// follow podman run/exec standard with the exit codes
if errors.Is(err, os.ErrNotExist) || errors.Is(err, exec.ErrNotFound) {
registry.SetExitCode(127)
} else if errors.Is(err, os.ErrPermission) {
registry.SetExitCode(126)
}
return err
}
return nil
return utils.HandleOSExecError(err)
}

View File

@ -4,10 +4,12 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
buildahCLI "github.com/containers/buildah/pkg/cli"
"github.com/containers/podman/v4/cmd/podman/registry"
)
type OutputErrors []error
@ -43,3 +45,33 @@ func ExitCodeFromBuildError(errorMsg string) (int, error) {
}
return buildahCLI.ExecErrorCodeGeneric, errors.New("message does not contains a valid exit code")
}
// HandleOSExecError checks the given error for an exec.ExitError error and
// sets the same podman exit code as the error.
// No error will be returned in this case to make sure things like podman
// unshare false work correctly without extra output.
// When the exec file does not exists we set the exit code to 127, for
// permission errors 126 is used as exit code. In this case we still return
// the error so the user gets an error message.
// If the error is nil it returns nil.
func HandleOSExecError(err error) error {
if err == nil {
return nil
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
// the user command inside the unshare/ssh env has failed
// we set the exit code, do not return the error to the user
// otherwise "exit status X" will be printed
registry.SetExitCode(exitError.ExitCode())
return nil
}
// cmd.Run() can return fs.ErrNotExist, fs.ErrPermission or exec.ErrNotFound
// follow podman run/exec standard with the exit codes
if errors.Is(err, os.ErrNotExist) || errors.Is(err, exec.ErrNotFound) {
registry.SetExitCode(127)
} else if errors.Is(err, os.ErrPermission) {
registry.SetExitCode(126)
}
return err
}

View File

@ -14,6 +14,7 @@ first argument must be the virtual machine name. The optional command to
execute can then follow. If no command is provided, an interactive session
with the virtual machine is established.
The exit code from ssh command will be forwarded to the podman machine ssh caller, see [Exit Codes](#Exit-Codes).
## OPTIONS
@ -25,6 +26,35 @@ Print usage statement.
Username to use when SSH-ing into the VM.
## Exit Codes
The exit code from `podman machine ssh` gives information about why the command failed.
When `podman machine ssh` commands exit with a non-zero code,
the exit codes follow the `chroot` standard, see below:
**125** The error is with podman **_itself_**
$ podman machine ssh --foo; echo $?
Error: unknown flag: --foo
125
**126** Executing a _contained command_ and the _command_ cannot be invoked
$ podman machine ssh /etc; echo $?
Error: fork/exec /etc: permission denied
126
**127** Executing a _contained command_ and the _command_ cannot be found
$ podman machine ssh foo; echo $?
Error: fork/exec /usr/bin/bogus: no such file or directory
127
**Exit code** _contained command_ exit code
$ podman machine ssh /bin/sh -c 'exit 3'; echo $?
3
## EXAMPLES
To get an interactive session with the default virtual machine:

View File

@ -85,6 +85,14 @@ func (ms *machineSession) outputToString() string {
return strings.Join(fields, " ")
}
// errorToString returns the error output from a session in string form
func (ms *machineSession) errorToString() string {
if ms == nil || ms.Err == nil || ms.Err.Contents() == nil {
return ""
}
return string(ms.Err.Contents())
}
// newMB constructor for machine test builders
func newMB() (*machineTestBuilder, error) {
mb := machineTestBuilder{

View File

@ -56,5 +56,12 @@ var _ = Describe("podman machine ssh", func() {
Expect(err).To(BeNil())
Expect(sshSession).To(Exit(0))
Expect(sshSession.outputToString()).To(ContainSubstring("Fedora CoreOS"))
// keep exit code
sshSession, err = mb.setName(name).setCmd(ssh.withSSHComand([]string{"false"})).run()
Expect(err).To(BeNil())
Expect(sshSession).To(Exit(1))
Expect(sshSession.outputToString()).To(Equal(""))
Expect(sshSession.errorToString()).To(Equal(""))
})
})