diff --git a/docs/website/docs/command-reference/run.md b/docs/website/docs/command-reference/run.md new file mode 100644 index 00000000000..60a9fbeb0ab --- /dev/null +++ b/docs/website/docs/command-reference/run.md @@ -0,0 +1,41 @@ +--- +title: odo run +--- + +`odo run` is used to manually execute commands defined in a Devfile. + +
+Example + +A command `connect` is defined in the Devfile, executing the `bash` command in the `runtime` component. + +```yaml +schemaVersion: 2.2.0 +[...] +commands: + - id: connect + exec: + component: runtime + commandLine: bash + [...] + +``` + +```shell +$ odo run connect +bash-4.4$ +``` + +
+ + +For `Exec` commands, `odo dev` needs to be running, and `odo run` +will execute commands in the containers deployed by the `odo dev` command. + +Standard input is redirected to the command running in the container, and the terminal is configured in Raw mode. For these reasons, any character will be redirected to the command in container, including the Ctrl-c character which can thus be used to interrupt the command in container. + +The `odo run` command terminates when the command in container terminates, and the exit status of `odo run` will reflect the exit status of the distant command: it will be `0` if the command in container terminates with status `0` and will be `1` if the command in container terminates with any other status. + +Resources deployed with `Apply` commands will be deployed in *Dev mode*, +and these resources will be deleted when `odo dev` terminates. + diff --git a/pkg/component/delete/delete.go b/pkg/component/delete/delete.go index 9756cd82dc1..26f1fb51af3 100644 --- a/pkg/component/delete/delete.go +++ b/pkg/component/delete/delete.go @@ -219,13 +219,14 @@ func (do *DeleteComponentClient) ExecutePreStopEvents(ctx context.Context, devfi do.kubeClient, do.execClient, do.configAutomountClient, - pod.Name, - false, - component.GetContainersNames(pod), - "Executing pre-stop command in container", + // TODO(feloy) set these values when we want to support Apply Image commands for PreStop events + nil, nil, - // TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PreStop events - nil, nil, parser.DevfileObj{}, "", + component.HandlerOptions{ + PodName: pod.Name, + ContainersRunning: component.GetContainersNames(pod), + Msg: "Executing pre-stop command in container", + }, ) err = libdevfile.ExecPreStopEvents(ctx, devfileObj, handler) if err != nil { diff --git a/pkg/component/execute_terminating.go b/pkg/component/execute_terminating.go index e3adf7f3645..faa9d9343fc 100644 --- a/pkg/component/execute_terminating.go +++ b/pkg/component/execute_terminating.go @@ -18,47 +18,67 @@ import ( const ShellExecutable string = "/bin/sh" -func ExecuteTerminatingCommand(ctx context.Context, execClient exec.Client, platformClient platform.Client, command devfilev1.Command, componentExists bool, podName string, appName string, componentName string, msg string, show bool) error { +func ExecuteTerminatingCommand( + ctx context.Context, + execClient exec.Client, + platformClient platform.Client, + command devfilev1.Command, + componentExists bool, + podName string, + appName string, + componentName string, + msg string, + directRun bool, +) error { if componentExists && command.Exec != nil && pointer.BoolDeref(command.Exec.HotReloadCapable, false) { klog.V(2).Infof("command is hot-reload capable, not executing %q again", command.Id) return nil } - if msg == "" { - msg = fmt.Sprintf("Executing %s command on container %q", command.Id, command.Exec.Component) - } else { - msg += " (command: " + command.Id + ")" - } - spinner := log.Spinner(msg) - defer spinner.End(false) + // Spinner is displayed only if no outputs are displayed + var spinner *log.Status + var stdoutWriter, stderrWriter *io.PipeWriter + var stdoutChannel, stderrChannel chan interface{} - logger := machineoutput.NewMachineEventLoggingClient() - stdoutWriter, stdoutChannel, stderrWriter, stderrChannel := logger.CreateContainerOutputWriter() + if !directRun { + if msg == "" { + msg = fmt.Sprintf("Executing %s command on container %q", command.Id, command.Exec.Component) + } else { + msg += " (command: " + command.Id + ")" + } + spinner = log.Spinner(msg) + defer spinner.End(false) - cmdline := getCmdline(command) - _, _, err := execClient.ExecuteCommand(ctx, cmdline, podName, command.Exec.Component, show, stdoutWriter, stderrWriter) + logger := machineoutput.NewMachineEventLoggingClient() + stdoutWriter, stdoutChannel, stderrWriter, stderrChannel = logger.CreateContainerOutputWriter() + } - closeWriterAndWaitForAck(stdoutWriter, stdoutChannel, stderrWriter, stderrChannel) + cmdline := getCmdline(command, !directRun) + _, _, err := execClient.ExecuteCommand(ctx, cmdline, podName, command.Exec.Component, directRun, stdoutWriter, stderrWriter) - spinner.End(err == nil) - if err != nil { - rd, errLog := Log(platformClient, componentName, appName, false, command) - if errLog != nil { - return fmt.Errorf("unable to log error %v: %w", err, errLog) - } + if !directRun { + closeWriterAndWaitForAck(stdoutWriter, stdoutChannel, stderrWriter, stderrChannel) + spinner.End(err == nil) + + if err != nil { + rd, errLog := Log(platformClient, componentName, appName, false, command) + if errLog != nil { + return fmt.Errorf("unable to log error %v: %w", err, errLog) + } - // Use GetStderr in order to make sure that colour output is correct - // on non-TTY terminals - errLog = util.DisplayLog(false, rd, log.GetStderr(), componentName, -1) - if errLog != nil { - return fmt.Errorf("unable to log error %v: %w", err, errLog) + // Use GetStderr in order to make sure that colour output is correct + // on non-TTY terminals + errLog = util.DisplayLog(false, rd, log.GetStderr(), componentName, -1) + if errLog != nil { + return fmt.Errorf("unable to log error %v: %w", err, errLog) + } } } return err } -func getCmdline(command v1alpha2.Command) []string { +func getCmdline(command v1alpha2.Command, redirectToPid1 bool) []string { // deal with environment variables var cmdLine string setEnvVariable := util.GetCommandStringFromEnvs(command.Exec.Env) @@ -73,7 +93,10 @@ func getCmdline(command v1alpha2.Command) []string { // Redirecting to /proc/1/fd/* allows to redirect the process output to the output streams of PID 1 process inside the container. // This way, returning the container logs with 'odo logs' or 'kubectl logs' would work seamlessly. // See https://stackoverflow.com/questions/58716574/where-exactly-do-the-logs-of-kubernetes-pods-come-from-at-the-container-level - redirectString := "1>>/proc/1/fd/1 2>>/proc/1/fd/2" + redirectString := "" + if redirectToPid1 { + redirectString = "1>>/proc/1/fd/1 2>>/proc/1/fd/2" + } var cmd []string if command.Exec.WorkingDir != "" { // since we are using /bin/sh -c, the command needs to be within a single double quote instance, for example "cd /tmp && pwd" diff --git a/pkg/component/handler.go b/pkg/component/handler.go index 2832eb2cd85..c5953cff70d 100644 --- a/pkg/component/handler.go +++ b/pkg/component/handler.go @@ -31,6 +31,7 @@ type runHandler struct { ComponentExists bool containersRunning []string msg string + directRun bool fs filesystem.Filesystem imageBackend image.Backend @@ -41,23 +42,29 @@ type runHandler struct { var _ libdevfile.Handler = (*runHandler)(nil) +type HandlerOptions struct { + PodName string + ComponentExists bool + ContainersRunning []string + Msg string + DirectRun bool + + // For apply Kubernetes / Openshift + Devfile parser.DevfileObj + Path string +} + func NewRunHandler( ctx context.Context, platformClient platform.Client, execClient exec.Client, configAutomountClient configAutomount.Client, - podName string, - componentExists bool, - containersRunning []string, - msg string, // For building images fs filesystem.Filesystem, imageBackend image.Backend, - // For apply Kubernetes / Openshift - devfile parser.DevfileObj, - path string, + options HandlerOptions, ) *runHandler { return &runHandler{ @@ -65,16 +72,17 @@ func NewRunHandler( platformClient: platformClient, execClient: execClient, configAutomountClient: configAutomountClient, - podName: podName, - ComponentExists: componentExists, - containersRunning: containersRunning, - msg: msg, + podName: options.PodName, + ComponentExists: options.ComponentExists, + containersRunning: options.ContainersRunning, + msg: options.Msg, + directRun: options.DirectRun, fs: fs, imageBackend: imageBackend, - devfile: devfile, - path: path, + devfile: options.Devfile, + path: options.Path, } } @@ -129,7 +137,7 @@ func (a *runHandler) ExecuteTerminatingCommand(ctx context.Context, command devf appName = odocontext.GetApplication(a.ctx) ) if isContainerRunning(command.Exec.Component, a.containersRunning) { - return ExecuteTerminatingCommand(ctx, a.execClient, a.platformClient, command, a.ComponentExists, a.podName, appName, componentName, a.msg, false) + return ExecuteTerminatingCommand(ctx, a.execClient, a.platformClient, command, a.ComponentExists, a.podName, appName, componentName, a.msg, a.directRun) } switch platform := a.platformClient.(type) { case kclient.ClientInterface: diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go index 65c2b4944ef..d342f8da22c 100644 --- a/pkg/deploy/deploy.go +++ b/pkg/deploy/deploy.go @@ -44,14 +44,12 @@ func (o *DeployClient) Deploy(ctx context.Context) error { o.kubeClient, nil, o.configAutomountClient, - "", - false, - nil, - "", o.fs, image.SelectBackend(ctx), - *devfileObj, - path, + component.HandlerOptions{ + Devfile: *devfileObj, + Path: path, + }, ) err := o.buildPushAutoImageComponents(handler, *devfileObj) diff --git a/pkg/dev/common/run.go b/pkg/dev/common/run.go index ddc89be77f4..54484cd3c9c 100644 --- a/pkg/dev/common/run.go +++ b/pkg/dev/common/run.go @@ -38,15 +38,16 @@ func Run( platformClient, execClient, configAutomountClient, - pod.Name, - false, - component.GetContainersNames(pod), - "Executing command in container", - filesystem, image.SelectBackend(ctx), - *devfileObj, - devfilePath, + component.HandlerOptions{ + PodName: pod.Name, + ContainersRunning: component.GetContainersNames(pod), + Msg: "Executing command in container", + DirectRun: true, + Devfile: *devfileObj, + Path: devfilePath, + }, ) return libdevfile.ExecuteCommandByName(ctx, *devfileObj, commandName, handler, false) diff --git a/pkg/dev/kubedev/innerloop.go b/pkg/dev/kubedev/innerloop.go index 1e050bfdd0e..1d0c4194159 100644 --- a/pkg/dev/kubedev/innerloop.go +++ b/pkg/dev/kubedev/innerloop.go @@ -7,7 +7,6 @@ import ( "time" devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - "github.com/devfile/library/v2/pkg/devfile/parser" parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" "github.com/redhat-developer/odo/pkg/component" @@ -95,13 +94,13 @@ func (o *DevClient) innerloop(ctx context.Context, parameters common.PushParamet o.kubernetesClient, o.execClient, o.configAutomountClient, - pod.Name, - false, - component.GetContainersNames(pod), - "Executing post-start command in container", - // TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PostStart commands - nil, nil, parser.DevfileObj{}, "", + nil, nil, + component.HandlerOptions{ + PodName: pod.Name, + ContainersRunning: component.GetContainersNames(pod), + Msg: "Executing post-start command in container", + }, ) err = libdevfile.ExecPostStartEvents(ctx, parameters.Devfile, handler) if err != nil { @@ -127,15 +126,14 @@ func (o *DevClient) innerloop(ctx context.Context, parameters common.PushParamet o.kubernetesClient, o.execClient, o.configAutomountClient, - pod.GetName(), - false, - component.GetContainersNames(pod), - "", - o.filesystem, image.SelectBackend(ctx), - parameters.Devfile, - path, + component.HandlerOptions{ + PodName: pod.GetName(), + ContainersRunning: component.GetContainersNames(pod), + Devfile: parameters.Devfile, + Path: path, + }, ) if commandType == devfilev1.ExecCommandType { @@ -166,13 +164,14 @@ func (o *DevClient) innerloop(ctx context.Context, parameters common.PushParamet o.kubernetesClient, o.execClient, o.configAutomountClient, - pod.Name, - running, - component.GetContainersNames(pod), - "Building your application in container", - // TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PostStart commands - nil, nil, parser.DevfileObj{}, "", + nil, nil, + component.HandlerOptions{ + PodName: pod.Name, + ComponentExists: running, + ContainersRunning: component.GetContainersNames(pod), + Msg: "Building your application in container", + }, ) return libdevfile.Build(ctx, parameters.Devfile, parameters.StartOptions.BuildCommand, execHandler) } diff --git a/pkg/dev/podmandev/reconcile.go b/pkg/dev/podmandev/reconcile.go index 5170b547000..2774865a63d 100644 --- a/pkg/dev/podmandev/reconcile.go +++ b/pkg/dev/podmandev/reconcile.go @@ -69,13 +69,13 @@ func (o *DevClient) reconcile( o.podmanClient, o.execClient, nil, // TODO(feloy) set this value when we want to support exec on new container on podman - pod.Name, - false, - component.GetContainersNames(pod), - "Executing post-start command in container", - // TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PostStart commands - nil, nil, parser.DevfileObj{}, "", + nil, nil, + component.HandlerOptions{ + PodName: pod.Name, + ContainersRunning: component.GetContainersNames(pod), + Msg: "Executing post-start command in container", + }, ) err = libdevfile.ExecPostStartEvents(ctx, devfileObj, execHandler) if err != nil { @@ -91,13 +91,14 @@ func (o *DevClient) reconcile( o.podmanClient, o.execClient, nil, // TODO(feloy) set this value when we want to support exec on new container on podman - pod.Name, - componentStatus.RunExecuted, - component.GetContainersNames(pod), - "Building your application in container", - // TODO(feloy) set these values when we want to support Apply Image/Kubernetes/OpenShift commands for PreStop events - nil, nil, parser.DevfileObj{}, "", + nil, nil, + component.HandlerOptions{ + PodName: pod.Name, + ComponentExists: componentStatus.RunExecuted, + ContainersRunning: component.GetContainersNames(pod), + Msg: "Building your application in container", + }, ) return libdevfile.Build(ctx, devfileObj, options.BuildCommand, execHandler) } @@ -119,16 +120,14 @@ func (o *DevClient) reconcile( o.podmanClient, o.execClient, nil, // TODO(feloy) set this value when we want to support exec on new container on podman - pod.Name, - componentStatus.RunExecuted, - component.GetContainersNames(pod), - "", - o.fs, image.SelectBackend(ctx), - // TODO(feloy) set to deploy Kubernetes/Openshift components - parser.DevfileObj{}, "", + component.HandlerOptions{ + PodName: pod.Name, + ComponentExists: componentStatus.RunExecuted, + ContainersRunning: component.GetContainersNames(pod), + }, ) err = libdevfile.ExecuteCommandByNameAndKind(ctx, devfileObj, cmdName, cmdKind, cmdHandler, false) if err != nil { diff --git a/pkg/exec/exec.go b/pkg/exec/exec.go index 49e32c52bc9..6538c4edf34 100644 --- a/pkg/exec/exec.go +++ b/pkg/exec/exec.go @@ -9,6 +9,7 @@ import ( "strings" "k8s.io/klog" + "k8s.io/kubectl/pkg/util/term" "github.com/redhat-developer/odo/pkg/log" "github.com/redhat-developer/odo/pkg/platform" @@ -26,46 +27,59 @@ func NewExecClient(platformClient platform.Client) *ExecClient { // ExecuteCommand executes the given command in the pod's container, // writing the output to the specified respective pipe writers -func (o ExecClient) ExecuteCommand(ctx context.Context, command []string, podName string, containerName string, show bool, stdoutWriter *io.PipeWriter, stderrWriter *io.PipeWriter) (stdout []string, stderr []string, err error) { - soutReader, soutWriter := io.Pipe() - serrReader, serrWriter := io.Pipe() - - klog.V(2).Infof("Executing command %v for pod: %v in container: %v", command, podName, containerName) - - // Read stdout and stderr, store their output in cmdOutput, and also pass output to consoleOutput Writers (if non-nil) - stdoutCompleteChannel := startReaderGoroutine(soutReader, show, &stdout, stdoutWriter) - stderrCompleteChannel := startReaderGoroutine(serrReader, show, &stderr, stderrWriter) - - err = o.platformClient.ExecCMDInContainer(ctx, containerName, podName, command, soutWriter, serrWriter, nil, false) +// when directRun is true, will execute the command with terminal in Raw mode and connected to local standard I/Os +// so input, including Ctrl-c, is sent to the remote process +func (o ExecClient) ExecuteCommand(ctx context.Context, command []string, podName string, containerName string, directRun bool, stdoutWriter *io.PipeWriter, stderrWriter *io.PipeWriter) (stdout []string, stderr []string, err error) { + if !directRun { + soutReader, soutWriter := io.Pipe() + serrReader, serrWriter := io.Pipe() + + klog.V(2).Infof("Executing command %v for pod: %v in container: %v", command, podName, containerName) + + // Read stdout and stderr, store their output in cmdOutput, and also pass output to consoleOutput Writers (if non-nil) + stdoutCompleteChannel := startReaderGoroutine(os.Stdout, soutReader, directRun, &stdout, stdoutWriter) + stderrCompleteChannel := startReaderGoroutine(os.Stderr, serrReader, directRun, &stderr, stderrWriter) + + err = o.platformClient.ExecCMDInContainer(ctx, containerName, podName, command, soutWriter, serrWriter, nil, false) + + // Block until we have received all the container output from each stream + _ = soutWriter.Close() + <-stdoutCompleteChannel + _ = serrWriter.Close() + <-stderrCompleteChannel + + // Details are displayed only if no outputs are displayed + if err != nil && !directRun { + // It is safe to read from stdout and stderr here, as the goroutines are guaranteed to have terminated at this point. + klog.V(2).Infof("ExecuteCommand returned an an err: %v. for command '%v'\nstdout: %v\nstderr: %v", + err, command, stdout, stderr) + + msg := fmt.Sprintf("unable to exec command %v", command) + if len(stdout) != 0 { + msg += fmt.Sprintf("\n=== stdout===\n%s", strings.Join(stdout, "\n")) + } + if len(stderr) != 0 { + msg += fmt.Sprintf("\n=== stderr===\n%s", strings.Join(stderr, "\n")) + } + return stdout, stderr, fmt.Errorf("%s: %w", msg, err) + } - // Block until we have received all the container output from each stream - _ = soutWriter.Close() - <-stdoutCompleteChannel - _ = serrWriter.Close() - <-stderrCompleteChannel + return stdout, stderr, err + } - if err != nil { - // It is safe to read from stdout and stderr here, as the goroutines are guaranteed to have terminated at this point. - klog.V(2).Infof("ExecuteCommand returned an an err: %v. for command '%v'\nstdout: %v\nstderr: %v", - err, command, stdout, stderr) + tty := setupTTY() - msg := fmt.Sprintf("unable to exec command %v", command) - if len(stdout) != 0 { - msg += fmt.Sprintf("\n=== stdout===\n%s", strings.Join(stdout, "\n")) - } - if len(stderr) != 0 { - msg += fmt.Sprintf("\n=== stderr===\n%s", strings.Join(stderr, "\n")) - } - return stdout, stderr, fmt.Errorf("%s: %w", msg, err) + fn := func() error { + return o.platformClient.ExecCMDInContainer(ctx, containerName, podName, command, tty.Out, os.Stderr, tty.In, tty.Raw) } - return stdout, stderr, err + return nil, nil, tty.Safe(fn) } // This goroutine will automatically pipe the output from the writer (passed into ExecCMDInContainer) to // the loggers. // The returned channel will contain a single nil entry once the reader has closed. -func startReaderGoroutine(reader io.Reader, show bool, cmdOutput *[]string, consoleOutput *io.PipeWriter) chan interface{} { +func startReaderGoroutine(logWriter io.Writer, reader io.Reader, show bool, cmdOutput *[]string, consoleOutput *io.PipeWriter) chan interface{} { result := make(chan interface{}) go func() { @@ -74,7 +88,7 @@ func startReaderGoroutine(reader io.Reader, show bool, cmdOutput *[]string, cons line := scanner.Text() if show { - _, err := fmt.Fprintln(os.Stdout, line) + _, err := fmt.Fprintln(logWriter, line) if err != nil { log.Errorf("Unable to print to stdout: %s", err.Error()) } @@ -98,3 +112,15 @@ func startReaderGoroutine(reader io.Reader, show bool, cmdOutput *[]string, cons return result } + +func setupTTY() term.TTY { + tty := term.TTY{ + In: os.Stdin, + Out: os.Stdout, + } + if !tty.IsTerminalIn() || !tty.IsTerminalOut() { + return tty + } + tty.Raw = true + return tty +} diff --git a/pkg/podman/exec.go b/pkg/podman/exec.go index 9f7b3279629..8f326bcf19d 100644 --- a/pkg/podman/exec.go +++ b/pkg/podman/exec.go @@ -23,13 +23,9 @@ func (o *PodmanCli) ExecCMDInContainer(ctx context.Context, containerName, podNa args = append(args, cmd...) command := exec.CommandContext(ctx, o.podmanCmd, append(o.containerRunGlobalExtraArgs, args...)...) - klog.V(3).Infof("executing %v", command.Args) + command.Stdout = stdout + command.Stderr = stderr command.Stdin = stdin - - out, err := command.Output() - if err != nil { - return err - } - _, err = stdout.Write(out) - return err + klog.V(3).Infof("executing %v", command.Args) + return command.Run() } diff --git a/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml b/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml index 2dfadf93705..ac3b1eb61f2 100644 --- a/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml +++ b/tests/examples/source/devfiles/nodejs/devfile-for-run.yaml @@ -68,19 +68,21 @@ commands: workingDir: ${PROJECTS_ROOT} group: kind: run - - id: create-file + - id: list-files exec: component: runtime - commandLine: touch /tmp/new-file - workingDir: ${PROJECTS_ROOT} - - id: create-file-in-other-container + commandLine: ls / + - id: list-files-in-other-container exec: component: other-container - commandLine: touch /tmp/new-file-in-other-container - workingDir: ${PROJECTS_ROOT} + commandLine: ls / - id: deploy-config apply: component: config - id: build-image apply: component: image + - id: error-cmd + exec: + component: runtime + commandLine: ls /not-found diff --git a/tests/integration/cmd_run_test.go b/tests/integration/cmd_run_test.go index 2aa2e50df1c..24bf914ac70 100644 --- a/tests/integration/cmd_run_test.go +++ b/tests/integration/cmd_run_test.go @@ -3,9 +3,7 @@ package integration import ( "path/filepath" - "github.com/redhat-developer/odo/pkg/labels" "github.com/redhat-developer/odo/tests/helper" - "k8s.io/utils/pointer" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -101,18 +99,14 @@ var _ = Describe("odo run command tests", func() { platform = "podman" } - By("executing an exec command", func() { - output := helper.Cmd("odo", "run", "create-file", "--platform", platform).ShouldPass().Out() - Expect(output).To(ContainSubstring("Executing command in container (command: create-file)")) - component := helper.NewComponent(cmpName, "app", labels.ComponentDevMode, commonVar.Project, commonVar.CliRunner) - component.Exec("runtime", []string{"ls", "/tmp/new-file"}, pointer.Bool(true)) + By("executing an exec command and displaying output", func() { + output := helper.Cmd("odo", "run", "list-files", "--platform", platform).ShouldPass().Out() + Expect(output).To(ContainSubstring("etc")) }) - By("executing an exec command in another container", func() { - output := helper.Cmd("odo", "run", "create-file-in-other-container", "--platform", platform).ShouldPass().Out() - Expect(output).To(ContainSubstring("Executing command in container (command: create-file-in-other-container)")) - component := helper.NewComponent(cmpName, "app", labels.ComponentDevMode, commonVar.Project, commonVar.CliRunner) - component.Exec("other-container", []string{"ls", "/tmp/new-file-in-other-container"}, pointer.Bool(true)) + By("executing an exec command in another container and displaying output", func() { + output := helper.Cmd("odo", "run", "list-files-in-other-container", "--platform", platform).ShouldPass().Out() + Expect(output).To(ContainSubstring("etc")) }) if !podman { @@ -140,6 +134,11 @@ var _ = Describe("odo run command tests", func() { }) } + + By("exiting with a status 1 when the exec command fails and displaying error output", func() { + out := helper.Cmd("odo", "run", "error-cmd", "--platform", platform).ShouldFail().Err() + Expect(out).To(ContainSubstring("No such file or directory")) + }) }) })) }