167 lines
5.0 KiB
Go
167 lines
5.0 KiB
Go
package tests_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cirruslabs/orchard/internal/execstream"
|
|
"github.com/cirruslabs/orchard/internal/tests/devcontroller"
|
|
"github.com/cirruslabs/orchard/internal/tests/platformdependent"
|
|
"github.com/cirruslabs/orchard/internal/tests/wait"
|
|
"github.com/cirruslabs/orchard/pkg/client"
|
|
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
|
"github.com/coder/websocket"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestVMExecWithoutStdin(t *testing.T) {
|
|
devClient, vmName := prepareForExec(t)
|
|
|
|
// Run a command
|
|
wsConn, err := devClient.VMs().Exec(t.Context(), vmName, "/bin/echo -n 'Hello, World!'",
|
|
false, 30)
|
|
require.NoError(t, err)
|
|
defer wsConn.CloseNow()
|
|
|
|
// Ensure that the command outputs "Hello, World!" and terminates successfully
|
|
frame := readFrame(t, wsConn)
|
|
require.Equal(t, execstream.FrameTypeStdout, frame.Type)
|
|
require.Equal(t, "Hello, World!", string(frame.Data))
|
|
|
|
frame = readFrame(t, wsConn)
|
|
require.Equal(t, execstream.FrameTypeExit, frame.Type)
|
|
require.EqualValues(t, 0, frame.Exit.Code)
|
|
|
|
// Ensure that Orchard Controller gracefully terminates the WebSocket connection
|
|
_, _, err = wsConn.Read(t.Context())
|
|
var closeError websocket.CloseError
|
|
require.ErrorAs(t, err, &closeError)
|
|
require.Equal(t, websocket.StatusNormalClosure, closeError.Code)
|
|
}
|
|
|
|
func TestVMExecWithStdin(t *testing.T) {
|
|
devClient, vmName := prepareForExec(t)
|
|
|
|
// Run a command
|
|
wsConn, err := devClient.VMs().Exec(t.Context(), vmName, "/bin/cat", true, 30)
|
|
require.NoError(t, err)
|
|
defer wsConn.CloseNow()
|
|
|
|
// Populate and close the command's standard input
|
|
err = execstream.WriteFrame(t.Context(), wsConn, &execstream.Frame{
|
|
Type: execstream.FrameTypeStdin,
|
|
Data: []byte("Hello, World!\n"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = execstream.WriteFrame(t.Context(), wsConn, &execstream.Frame{
|
|
Type: execstream.FrameTypeStdin,
|
|
Data: []byte{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Ensure that the command outputs "Hello, World!\n" and terminates successfully
|
|
frame := readFrame(t, wsConn)
|
|
require.Equal(t, execstream.FrameTypeStdout, frame.Type)
|
|
require.Equal(t, "Hello, World!\n", string(frame.Data))
|
|
|
|
frame = readFrame(t, wsConn)
|
|
require.Equal(t, execstream.FrameTypeExit, frame.Type)
|
|
require.EqualValues(t, 0, frame.Exit.Code)
|
|
|
|
// Ensure that Orchard Controller gracefully terminates the WebSocket connection
|
|
_, _, err = wsConn.Read(t.Context())
|
|
var closeError websocket.CloseError
|
|
require.ErrorAs(t, err, &closeError)
|
|
require.Equal(t, websocket.StatusNormalClosure, closeError.Code)
|
|
}
|
|
|
|
func TestVMExecScript(t *testing.T) {
|
|
devClient, vmName := prepareForExec(t)
|
|
|
|
script := "sh -c 'echo stdout-line; echo stderr-line >&2; IFS= read -r line; echo stdin:$line; exit 7'"
|
|
|
|
wsConn, err := devClient.VMs().Exec(t.Context(), vmName, script, true, 30)
|
|
require.NoError(t, err)
|
|
defer wsConn.CloseNow()
|
|
|
|
// Populate and close the command's standard input
|
|
err = execstream.WriteFrame(t.Context(), wsConn, &execstream.Frame{
|
|
Type: execstream.FrameTypeStdin,
|
|
Data: []byte("hello-from-test\n"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = execstream.WriteFrame(t.Context(), wsConn, &execstream.Frame{
|
|
Type: execstream.FrameTypeStdin,
|
|
Data: []byte{},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Collect output and wait for command's exit
|
|
var stdout, stderr bytes.Buffer
|
|
var exitFrame *execstream.Frame
|
|
|
|
for exitFrame == nil {
|
|
frame := readFrame(t, wsConn)
|
|
|
|
switch frame.Type {
|
|
case execstream.FrameTypeStdout:
|
|
stdout.Write(frame.Data)
|
|
case execstream.FrameTypeStderr:
|
|
stderr.Write(frame.Data)
|
|
case execstream.FrameTypeExit:
|
|
exitFrame = frame
|
|
default:
|
|
t.Fatalf("unexpected frame type %q", frame.Type)
|
|
}
|
|
}
|
|
|
|
// Ensure that we've observed everything as per script
|
|
require.EqualValues(t, 7, exitFrame.Exit.Code)
|
|
require.Equal(t, "stdout-line\nstdin:hello-from-test\n", stdout.String())
|
|
require.Equal(t, "stderr-line\n", stderr.String())
|
|
|
|
// Ensure that Orchard Controller gracefully terminates the WebSocket connection
|
|
_, _, err = wsConn.Read(t.Context())
|
|
var closeError websocket.CloseError
|
|
require.ErrorAs(t, err, &closeError)
|
|
require.Equal(t, websocket.StatusNormalClosure, closeError.Code)
|
|
}
|
|
|
|
func prepareForExec(t *testing.T) (*client.Client, string) {
|
|
devClient, _, _ := devcontroller.StartIntegrationTestEnvironment(t)
|
|
|
|
vmName := "test-vm-exec-" + uuid.NewString()
|
|
|
|
err := devClient.VMs().Create(t.Context(), platformdependent.VM(vmName))
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, wait.Wait(2*time.Minute, func() bool {
|
|
vm, err := devClient.VMs().Get(t.Context(), vmName)
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("Waiting for the VM to start. Current status: %s", vm.Status)
|
|
|
|
return vm.Status == v1.VMStatusRunning
|
|
}), "failed to start a VM")
|
|
|
|
return devClient, vmName
|
|
}
|
|
|
|
func readFrame(t *testing.T, wsConn *websocket.Conn) *execstream.Frame {
|
|
var frame execstream.Frame
|
|
|
|
messageType, payloadBytes, err := wsConn.Read(t.Context())
|
|
require.NoError(t, err)
|
|
require.Equal(t, websocket.MessageText, messageType)
|
|
|
|
err = json.Unmarshal(payloadBytes, &frame)
|
|
require.NoError(t, err)
|
|
|
|
return &frame
|
|
}
|