Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Add interactive vm exec support #572

Merged
merged 1 commit into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion cmd/ignite/cmd/vmcmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ func NewCmdExec(out io.Writer, err io.Writer, in io.Reader) *cobra.Command {

func addExecFlags(fs *pflag.FlagSet, ef *run.ExecFlags) {
fs.StringVarP(&ef.IdentityFile, "identity", "i", "", "Override the vm's default identity file")
fs.Uint32VarP(&ef.Timeout, "timeout", "t", 10, "Timeout waiting for connection in seconds")
fs.Uint32Var(&ef.Timeout, "timeout", 10, "Timeout waiting for connection in seconds")
fs.BoolVarP(&ef.Tty, "tty", "t", false, "Allocate a pseudo-TTY")
}
45 changes: 19 additions & 26 deletions cmd/ignite/run/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package run

import (
"fmt"
"io"
"io/ioutil"
"net"
"os"
Expand All @@ -16,9 +15,11 @@ import (
shellescape "gopkg.in/alessio/shellescape.v1"
)

// ExecFlags contains the flags supported by the exec command.
type ExecFlags struct {
Timeout uint32
IdentityFile string
Tty bool
}

type execOptions struct {
Expand All @@ -27,6 +28,7 @@ type execOptions struct {
command []string
}

// NewExecOptions constructs and returns an execOptions.
func (ef *ExecFlags) NewExecOptions(vmMatch string, command ...string) (eo *execOptions, err error) {
eo = &execOptions{
ExecFlags: ef,
Expand All @@ -37,6 +39,7 @@ func (ef *ExecFlags) NewExecOptions(vmMatch string, command ...string) (eo *exec
return
}

// Exec executes command in a VM based on the provided execOptions.
func Exec(eo *execOptions) error {
// Check if the VM is running
if !eo.vm.Running() {
Expand Down Expand Up @@ -72,7 +75,7 @@ func Exec(eo *execOptions) error {

// Run the command, DO NOT wrap this error as the caller can check for the command exit
// code in the ssh.ExitError type
return runSSHCommand(client, eo.command)
return runSSHCommand(client, eo.Tty, eo.command)
}

func newSignerForKey(keyPath string) (ssh.Signer, error) {
Expand All @@ -96,40 +99,30 @@ func newSSHConfig(publicKey ssh.Signer, timeout uint32) *ssh.ClientConfig {
}
}

func runSSHCommand(client *ssh.Client, command []string) error {
func runSSHCommand(client *ssh.Client, tty bool, command []string) error {
// create a session for the command
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("failed to create session: %v", err)
}
defer session.Close()

// get a pty
// TODO: should these be based on the host terminal?
// TODO: should we request something other than xterm?
// TODO: we should probably configure the terminal modes
modes := ssh.TerminalModes{}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
return fmt.Errorf("request for pseudo terminal failed: %v", err)
if tty {
// get a pty
// TODO: should these be based on the host terminal?
// TODO: should we request something other than xterm?
// TODO: we should probably configure the terminal modes
modes := ssh.TerminalModes{}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
return fmt.Errorf("request for pseudo terminal failed: %v", err)
}
}

// connect input / output
// Connect input / output
// TODO: these should come from the cobra command instead of hardcoding os.Stderr etc.
stderr, err := session.StderrPipe()
if err != nil {
return fmt.Errorf("failed to connect stderr: %v", err)
}
go io.Copy(os.Stderr, stderr)
stdout, err := session.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to connect stdout: %v", err)
}
go io.Copy(os.Stdout, stdout)
stdin, err := session.StdinPipe()
if err != nil {
return fmt.Errorf("failed to connect stdin: %v", err)
}
go io.Copy(stdin, os.Stdin)
session.Stderr = os.Stderr
session.Stdout = os.Stdout
session.Stdin = os.Stdin

/*
Do not wrap this error so the caller can check for the exit code
Expand Down
3 changes: 2 additions & 1 deletion docs/cli/ignite/ignite_exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ ignite exec <vm> <command...> [flags]
```
-h, --help help for exec
-i, --identity string Override the vm's default identity file
-t, --timeout uint32 Timeout waiting for connection in seconds (default 10)
--timeout uint32 Timeout waiting for connection in seconds (default 10)
-t, --tty Allocate a pseudo-TTY
```

### Options inherited from parent commands
Expand Down
60 changes: 60 additions & 0 deletions e2e/vm_exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package e2e

import (
"fmt"
"os/exec"
"strings"
"testing"

"gotest.tools/assert"
)

func TestVMExecInteractive(t *testing.T) {
assert.Assert(t, e2eHome != "", "IGNITE_E2E_HOME should be set")

vmName := "e2e_test_ignite_exec_interactive"

runCmd := exec.Command(
igniteBin,
"run", "--name="+vmName,
"--ssh",
"weaveworks/ignite-ubuntu",
)
runOut, runErr := runCmd.CombinedOutput()

defer func() {
rmvCmd := exec.Command(
igniteBin,
"rm", "-f", vmName,
)
rmvOut, rmvErr := rmvCmd.CombinedOutput()
assert.Check(t, rmvErr, fmt.Sprintf("vm removal: \n%q\n%s", rmvCmd.Args, rmvOut))
}()

assert.Check(t, runErr, fmt.Sprintf("vm run: \n%q\n%s", runCmd.Args, runOut))

// Pass input data from host and write to a file inside the VM.
remoteFileName := "afile.txt"
inputContent := "foooo..."
input := strings.NewReader(inputContent)

execCmd := exec.Command(
igniteBin,
"exec", vmName,
"tee", remoteFileName,
)
execCmd.Stdin = input

execOut, execErr := execCmd.CombinedOutput()
assert.Check(t, execErr, fmt.Sprintf("exec: \n%q\n%s", execCmd.Args, execOut))

// Check the file content inside the VM.
catCmd := exec.Command(
igniteBin,
"exec", vmName,
"cat", remoteFileName,
)
catOut, catErr := catCmd.CombinedOutput()
assert.Check(t, catErr, fmt.Sprintf("cat: \n%q\n%s", catCmd.Args, catOut))
assert.Equal(t, string(catOut), inputContent, fmt.Sprintf("unexpected file content on host:\n\t(WNT): %q\n\t(GOT): %q", inputContent, string(catOut)))
}