Parametrize e2e tests to also check AWS environments.

- Fix destroy runner after timeout.
- Add file deletion
This commit is contained in:
Maximilian Paß
2022-02-03 14:32:05 +01:00
parent 13eaa61f3b
commit 4ffbb712ed
13 changed files with 505 additions and 342 deletions

View File

@ -106,6 +106,10 @@ jobs:
e2e-test: e2e-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ compile, dep-scan, test ] needs: [ compile, dep-scan, test ]
env:
POSEIDON_AWS_ENABLED: true
POSEIDON_AWS_ENDPOINT: ${{ secrets.POSEIDON_AWS_ENDPOINT }}
POSEIDON_AWS_FUNCTIONS: ${{ secrets.POSEIDON_AWS_FUNCTIONS }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@ -8,12 +8,13 @@ import (
) )
type AWSEnvironment struct { type AWSEnvironment struct {
id dto.EnvironmentID id dto.EnvironmentID
awsEndpoint string awsEndpoint string
onDestroyRunner runner.DestroyRunnerHandler
} }
func NewAWSEnvironment() *AWSEnvironment { func NewAWSEnvironment(onDestroyRunner runner.DestroyRunnerHandler) *AWSEnvironment {
return &AWSEnvironment{} return &AWSEnvironment{onDestroyRunner: onDestroyRunner}
} }
func (a *AWSEnvironment) MarshalJSON() ([]byte, error) { func (a *AWSEnvironment) MarshalJSON() ([]byte, error) {
@ -86,11 +87,11 @@ func (a *AWSEnvironment) Register() error {
} }
func (a *AWSEnvironment) Delete() error { func (a *AWSEnvironment) Delete() error {
panic("implement me") return nil
} }
func (a *AWSEnvironment) Sample() (r runner.Runner, ok bool) { func (a *AWSEnvironment) Sample() (r runner.Runner, ok bool) {
workload, err := runner.NewAWSFunctionWorkload(a, nil) workload, err := runner.NewAWSFunctionWorkload(a, a.onDestroyRunner)
if err != nil { if err != nil {
return nil, false return nil, false
} }

View File

@ -53,7 +53,7 @@ func (a *AWSEnvironmentManager) CreateOrUpdate(
} }
_, ok := a.runnerManager.GetEnvironment(id) _, ok := a.runnerManager.GetEnvironment(id)
e := NewAWSEnvironment() e := NewAWSEnvironment(a.runnerManager.Return)
e.SetID(id) e.SetID(id)
e.SetImage(request.Image) e.SetImage(request.Image)
a.runnerManager.StoreEnvironment(e) a.runnerManager.StoreEnvironment(e)

View File

@ -66,7 +66,7 @@ func TestAWSEnvironmentManager_Get(t *testing.T) {
}) })
t.Run("Returns environment when it was added before", func(t *testing.T) { t.Run("Returns environment when it was added before", func(t *testing.T) {
expectedEnvironment := NewAWSEnvironment() expectedEnvironment := NewAWSEnvironment(nil)
expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger) expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
runnerManager.StoreEnvironment(expectedEnvironment) runnerManager.StoreEnvironment(expectedEnvironment)
@ -82,7 +82,7 @@ func TestAWSEnvironmentManager_List(t *testing.T) {
t.Run("returs also environments of the rest of the manager chain", func(t *testing.T) { t.Run("returs also environments of the rest of the manager chain", func(t *testing.T) {
nextHandler := &ManagerHandlerMock{} nextHandler := &ManagerHandlerMock{}
existingEnvironment := NewAWSEnvironment() existingEnvironment := NewAWSEnvironment(nil)
nextHandler.On("List", mock.AnythingOfType("bool")). nextHandler.On("List", mock.AnythingOfType("bool")).
Return([]runner.ExecutionEnvironment{existingEnvironment}, nil) Return([]runner.ExecutionEnvironment{existingEnvironment}, nil)
m.SetNextHandler(nextHandler) m.SetNextHandler(nextHandler)
@ -95,7 +95,7 @@ func TestAWSEnvironmentManager_List(t *testing.T) {
m.SetNextHandler(nil) m.SetNextHandler(nil)
t.Run("Returns added environment", func(t *testing.T) { t.Run("Returns added environment", func(t *testing.T) {
localEnvironment := NewAWSEnvironment() localEnvironment := NewAWSEnvironment(nil)
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger) localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
runnerManager.StoreEnvironment(localEnvironment) runnerManager.StoreEnvironment(localEnvironment)

View File

@ -24,29 +24,33 @@ type awsFunctionRequest struct {
// AWSFunctionWorkload is an abstraction to build a request to an AWS Lambda Function. // AWSFunctionWorkload is an abstraction to build a request to an AWS Lambda Function.
type AWSFunctionWorkload struct { type AWSFunctionWorkload struct {
InactivityTimer InactivityTimer
id string id string
fs map[dto.FilePath][]byte fs map[dto.FilePath][]byte
executions execution.Storer executions execution.Storer
onDestroy destroyRunnerHandler runningExecutions map[execution.ID]context.CancelFunc
environment ExecutionEnvironment onDestroy DestroyRunnerHandler
environment ExecutionEnvironment
} }
// NewAWSFunctionWorkload creates a new AWSFunctionWorkload with the provided id. // NewAWSFunctionWorkload creates a new AWSFunctionWorkload with the provided id.
func NewAWSFunctionWorkload( func NewAWSFunctionWorkload(
environment ExecutionEnvironment, onDestroy destroyRunnerHandler) (*AWSFunctionWorkload, error) { environment ExecutionEnvironment, onDestroy DestroyRunnerHandler) (*AWSFunctionWorkload, error) {
newUUID, err := uuid.NewUUID() newUUID, err := uuid.NewUUID()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed generating runner id: %w", err) return nil, fmt.Errorf("failed generating runner id: %w", err)
} }
workload := &AWSFunctionWorkload{ workload := &AWSFunctionWorkload{
id: newUUID.String(), id: newUUID.String(),
fs: make(map[dto.FilePath][]byte), fs: make(map[dto.FilePath][]byte),
executions: execution.NewLocalStorage(), executions: execution.NewLocalStorage(),
onDestroy: onDestroy, runningExecutions: make(map[execution.ID]context.CancelFunc),
environment: environment, onDestroy: onDestroy,
environment: environment,
} }
workload.InactivityTimer = NewInactivityTimer(workload, onDestroy) workload.InactivityTimer = NewInactivityTimer(workload, func(_ Runner) error {
return workload.Destroy()
})
return workload, nil return workload, nil
} }
@ -73,16 +77,21 @@ func (w *AWSFunctionWorkload) ExecuteInteractively(id string, _ io.ReadWriter, s
if !ok { if !ok {
return nil, nil, ErrorUnknownExecution return nil, nil, ErrorUnknownExecution
} }
command, ctx, cancel := prepareExecution(request) command, ctx, cancel := prepareExecution(request)
exitInternal := make(chan ExitInfo)
exit := make(chan ExitInfo, 1) exit := make(chan ExitInfo, 1)
go w.executeCommand(ctx, command, stdout, stderr, exit)
go w.executeCommand(ctx, command, stdout, stderr, exitInternal)
go w.handleRunnerTimeout(ctx, exitInternal, exit, execution.ID(id))
return exit, cancel, nil return exit, cancel, nil
} }
// UpdateFileSystem copies Files into the executor. // UpdateFileSystem copies Files into the executor.
// ToDo: Currently, file deletion is not supported (but it could be).
func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequest) error { func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequest) error {
for _, path := range request.Delete {
delete(w.fs, path)
}
for _, file := range request.Copy { for _, file := range request.Copy {
w.fs[file.Path] = file.Content w.fs[file.Path] = file.Content
} }
@ -90,6 +99,9 @@ func (w *AWSFunctionWorkload) UpdateFileSystem(request *dto.UpdateFileSystemRequ
} }
func (w *AWSFunctionWorkload) Destroy() error { func (w *AWSFunctionWorkload) Destroy() error {
for _, cancel := range w.runningExecutions {
cancel()
}
if err := w.onDestroy(w); err != nil { if err := w.onDestroy(w); err != nil {
return fmt.Errorf("error while destroying aws runner: %w", err) return fmt.Errorf("error while destroying aws runner: %w", err)
} }
@ -99,6 +111,7 @@ func (w *AWSFunctionWorkload) Destroy() error {
func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []string, func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []string,
stdout, stderr io.Writer, exit chan<- ExitInfo, stdout, stderr io.Writer, exit chan<- ExitInfo,
) { ) {
defer close(exit)
data := &awsFunctionRequest{ data := &awsFunctionRequest{
Action: w.environment.Image(), Action: w.environment.Image(),
Cmd: command, Cmd: command,
@ -128,7 +141,6 @@ func (w *AWSFunctionWorkload) executeCommand(ctx context.Context, command []stri
err = ErrorRunnerInactivityTimeout err = ErrorRunnerInactivityTimeout
} }
exit <- ExitInfo{exitCode, err} exit <- ExitInfo{exitCode, err}
close(exit)
} }
func (w *AWSFunctionWorkload) receiveOutput( func (w *AWSFunctionWorkload) receiveOutput(
@ -157,7 +169,7 @@ func (w *AWSFunctionWorkload) receiveOutput(
case dto.WebSocketOutputStdout: case dto.WebSocketOutputStdout:
// We do not check the written bytes as the rawToCodeOceanWriter receives everything or nothing. // We do not check the written bytes as the rawToCodeOceanWriter receives everything or nothing.
_, err = stdout.Write([]byte(wsMessage.Data)) _, err = stdout.Write([]byte(wsMessage.Data))
case dto.WebSocketOutputStderr: case dto.WebSocketOutputStderr, dto.WebSocketOutputError:
_, err = stderr.Write([]byte(wsMessage.Data)) _, err = stderr.Write([]byte(wsMessage.Data))
} }
if err != nil { if err != nil {
@ -166,3 +178,20 @@ func (w *AWSFunctionWorkload) receiveOutput(
} }
return 1, fmt.Errorf("receiveOutput stpped by context: %w", ctx.Err()) return 1, fmt.Errorf("receiveOutput stpped by context: %w", ctx.Err())
} }
// handleRunnerTimeout listens for a runner timeout and aborts the execution in that case.
// It listens via a context in runningExecutions that is canceled on the timeout event.
func (w *AWSFunctionWorkload) handleRunnerTimeout(ctx context.Context,
exitInternal <-chan ExitInfo, exit chan<- ExitInfo, id execution.ID) {
executionCtx, cancelExecution := context.WithCancel(ctx)
w.runningExecutions[id] = cancelExecution
defer delete(w.runningExecutions, id)
defer close(exit)
select {
case exitInfo := <-exitInternal:
exit <- exitInfo
case <-executionCtx.Done():
exit <- ExitInfo{255, ErrorRunnerInactivityTimeout}
}
}

View File

@ -37,11 +37,11 @@ type InactivityTimerImplementation struct {
duration time.Duration duration time.Duration
state TimerState state TimerState
runner Runner runner Runner
onDestroy destroyRunnerHandler onDestroy DestroyRunnerHandler
mu sync.Mutex mu sync.Mutex
} }
func NewInactivityTimer(runner Runner, onDestroy destroyRunnerHandler) InactivityTimer { func NewInactivityTimer(runner Runner, onDestroy DestroyRunnerHandler) InactivityTimer {
return &InactivityTimerImplementation{ return &InactivityTimerImplementation{
state: TimerInactive, state: TimerInactive,
runner: runner, runner: runner,

View File

@ -41,12 +41,12 @@ type NomadJob struct {
id string id string
portMappings []nomadApi.PortMapping portMappings []nomadApi.PortMapping
api nomad.ExecutorAPI api nomad.ExecutorAPI
onDestroy destroyRunnerHandler onDestroy DestroyRunnerHandler
} }
// NewNomadJob creates a new NomadJob with the provided id. // NewNomadJob creates a new NomadJob with the provided id.
func NewNomadJob(id string, portMappings []nomadApi.PortMapping, func NewNomadJob(id string, portMappings []nomadApi.PortMapping,
apiClient nomad.ExecutorAPI, onDestroy destroyRunnerHandler, apiClient nomad.ExecutorAPI, onDestroy DestroyRunnerHandler,
) *NomadJob { ) *NomadJob {
job := &NomadJob{ job := &NomadJob{
id: id, id: id,

View File

@ -391,7 +391,7 @@ func (s *UpdateFileSystemTestSuite) readFilesFromTarArchive(tarArchive io.Reader
// NewRunner creates a new runner with the provided id and manager. // NewRunner creates a new runner with the provided id and manager.
func NewRunner(id string, manager Accessor) Runner { func NewRunner(id string, manager Accessor) Runner {
var handler destroyRunnerHandler var handler DestroyRunnerHandler
if manager != nil { if manager != nil {
handler = manager.Return handler = manager.Return
} else { } else {

View File

@ -11,7 +11,7 @@ type ExitInfo struct {
Err error Err error
} }
type destroyRunnerHandler = func(r Runner) error type DestroyRunnerHandler = func(r Runner) error
type Runner interface { type Runner interface {
InactivityTimer InactivityTimer

View File

@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"net/http" "net/http"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
) )
@ -27,6 +28,7 @@ var (
testDockerImage = flag.String("dockerImage", "", "Docker image to use in E2E tests") testDockerImage = flag.String("dockerImage", "", "Docker image to use in E2E tests")
nomadClient *nomadApi.Client nomadClient *nomadApi.Client
nomadNamespace string nomadNamespace string
environmentIDs []dto.EnvironmentID
) )
type E2ETestSuite struct { type E2ETestSuite struct {
@ -45,11 +47,42 @@ func TestE2ETestSuite(t *testing.T) {
// Overwrite TestMain for custom setup. // Overwrite TestMain for custom setup.
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
log.Info("Test Setup") log.Info("Test Setup")
err := config.InitConfig() if err := config.InitConfig(); err != nil {
if err != nil {
log.WithError(err).Fatal("Could not initialize configuration") log.WithError(err).Fatal("Could not initialize configuration")
} }
initNomad()
initAWS()
// wait for environment to become ready
<-time.After(10 * time.Second)
log.Info("Test Run")
code := m.Run()
deleteE2EEnvironments()
cleanupJobsForEnvironment(&testing.T{}, tests.DefaultEnvironmentIDAsString)
os.Exit(code)
}
func initAWS() {
for i, function := range strings.Fields(config.Config.AWS.Functions) {
id := dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger + i + 1)
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, id.ToString())
request := dto.ExecutionEnvironmentRequest{Image: function}
resp, err := helpers.HTTPPutJSON(path, request)
if err != nil || resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
log.WithField("function", function).WithError(err).Fatal("Couldn't create default environment for e2e tests")
}
environmentIDs = append(environmentIDs, id)
err = resp.Body.Close()
if err != nil {
log.Fatal("Failed closing body")
}
}
}
func initNomad() {
nomadNamespace = config.Config.Nomad.Namespace nomadNamespace = config.Config.Nomad.Namespace
var err error
nomadClient, err = nomadApi.NewClient(&nomadApi.Config{ nomadClient, err = nomadApi.NewClient(&nomadApi.Config{
Address: config.Config.Nomad.URL().String(), Address: config.Config.Nomad.URL().String(),
TLSConfig: &nomadApi.TLSConfig{}, TLSConfig: &nomadApi.TLSConfig{},
@ -57,16 +90,9 @@ func TestMain(m *testing.M) {
}) })
if err != nil { if err != nil {
log.WithError(err).Fatal("Could not create Nomad client") log.WithError(err).Fatal("Could not create Nomad client")
return
} }
log.Info("Test Run")
createDefaultEnvironment() createDefaultEnvironment()
// wait for environment to become ready
<-time.After(10 * time.Second)
code := m.Run()
cleanupJobsForEnvironment(&testing.T{}, tests.DefaultEnvironmentIDAsString)
os.Exit(code)
} }
func createDefaultEnvironment() { func createDefaultEnvironment() {
@ -89,8 +115,15 @@ func createDefaultEnvironment() {
if err != nil || resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { if err != nil || resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
log.WithError(err).Fatal("Couldn't create default environment for e2e tests") log.WithError(err).Fatal("Couldn't create default environment for e2e tests")
} }
environmentIDs = append(environmentIDs, tests.DefaultEnvironmentIDAsInteger)
err = resp.Body.Close() err = resp.Body.Close()
if err != nil { if err != nil {
log.Fatal("Failed closing body") log.Fatal("Failed closing body")
} }
} }
func deleteE2EEnvironments() {
for _, id := range environmentIDs {
deleteEnvironment(&testing.T{}, id.ToString())
}
}

View File

@ -2,8 +2,10 @@ package e2e
import ( import (
"encoding/json" "encoding/json"
"fmt"
nomadApi "github.com/hashicorp/nomad/api" nomadApi "github.com/hashicorp/nomad/api"
"github.com/openHPI/poseidon/internal/api" "github.com/openHPI/poseidon/internal/api"
"github.com/openHPI/poseidon/internal/config"
"github.com/openHPI/poseidon/internal/nomad" "github.com/openHPI/poseidon/internal/nomad"
"github.com/openHPI/poseidon/pkg/dto" "github.com/openHPI/poseidon/pkg/dto"
"github.com/openHPI/poseidon/tests" "github.com/openHPI/poseidon/tests"
@ -17,6 +19,8 @@ import (
"time" "time"
) )
var isAWSEnvironment = []bool{false, true}
func TestCreateOrUpdateEnvironment(t *testing.T) { func TestCreateOrUpdateEnvironment(t *testing.T) {
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString)
@ -73,13 +77,13 @@ func TestCreateOrUpdateEnvironment(t *testing.T) {
func TestListEnvironments(t *testing.T) { func TestListEnvironments(t *testing.T) {
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath) path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath)
t.Run("returns list with one element", func(t *testing.T) { t.Run("returns list with all static and the e2e environment", func(t *testing.T) {
response, err := http.Get(path) //nolint:gosec // because we build this path right above response, err := http.Get(path) //nolint:gosec // because we build this path right above
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, http.StatusOK, response.StatusCode) assert.Equal(t, http.StatusOK, response.StatusCode)
environmentsArray := assertEnvironmentArrayInResponse(t, response) environmentsArray := assertEnvironmentArrayInResponse(t, response)
assert.Equal(t, 1, len(environmentsArray)) assert.Equal(t, len(environmentIDs), len(environmentsArray))
}) })
t.Run("returns list including the default environment", func(t *testing.T) { t.Run("returns list including the default environment", func(t *testing.T) {
@ -88,24 +92,28 @@ func TestListEnvironments(t *testing.T) {
require.Equal(t, http.StatusOK, response.StatusCode) require.Equal(t, http.StatusOK, response.StatusCode)
environmentsArray := assertEnvironmentArrayInResponse(t, response) environmentsArray := assertEnvironmentArrayInResponse(t, response)
require.Equal(t, 1, len(environmentsArray)) require.Equal(t, len(environmentIDs), len(environmentsArray))
assertEnvironment(t, environmentsArray[0], tests.DefaultEnvironmentIDAsInteger)
})
t.Run("Added environments can be retrieved without fetch", func(t *testing.T) {
createEnvironment(t, tests.AnotherEnvironmentIDAsString)
response, err := http.Get(path) //nolint:gosec // because we build this path right above
require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode)
environmentsArray := assertEnvironmentArrayInResponse(t, response)
require.Equal(t, 2, len(environmentsArray))
foundIDs := parseIDsFromEnvironments(t, environmentsArray) foundIDs := parseIDsFromEnvironments(t, environmentsArray)
assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)) assert.Contains(t, foundIDs, dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
}) })
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
for _, useAWS := range isAWSEnvironment {
t.Run(fmt.Sprintf("AWS-%t", useAWS), func(t *testing.T) {
t.Run("Added environments can be retrieved without fetch", func(t *testing.T) {
createEnvironment(t, tests.AnotherEnvironmentIDAsString, useAWS)
response, err := http.Get(path) //nolint:gosec // because we build this path right above
require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode)
environmentsArray := assertEnvironmentArrayInResponse(t, response)
require.Equal(t, len(environmentIDs)+1, len(environmentsArray))
foundIDs := parseIDsFromEnvironments(t, environmentsArray)
assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger))
})
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
})
}
t.Run("Added environments can be retrieved with fetch", func(t *testing.T) { t.Run("Added environments can be retrieved with fetch", func(t *testing.T) {
// Add environment without Poseidon // Add environment without Poseidon
@ -122,16 +130,17 @@ func TestListEnvironments(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode) require.Equal(t, http.StatusOK, response.StatusCode)
environmentsArray := assertEnvironmentArrayInResponse(t, response) environmentsArray := assertEnvironmentArrayInResponse(t, response)
require.Equal(t, 1, len(environmentsArray)) require.Equal(t, len(environmentIDs), len(environmentsArray))
assertEnvironment(t, environmentsArray[0], tests.DefaultEnvironmentIDAsInteger) foundIDs := parseIDsFromEnvironments(t, environmentsArray)
assert.Contains(t, foundIDs, dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger))
// List with fetch should include the added environment // List with fetch should include the added environment
response, err = http.Get(path + "?fetch=true") //nolint:gosec // because we build this path right above response, err = http.Get(path + "?fetch=true") //nolint:gosec // because we build this path right above
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode) require.Equal(t, http.StatusOK, response.StatusCode)
environmentsArray = assertEnvironmentArrayInResponse(t, response) environmentsArray = assertEnvironmentArrayInResponse(t, response)
require.Equal(t, 2, len(environmentsArray)) require.Equal(t, len(environmentIDs)+1, len(environmentsArray))
foundIDs := parseIDsFromEnvironments(t, environmentsArray) foundIDs = parseIDsFromEnvironments(t, environmentsArray)
assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)) assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger))
}) })
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
@ -148,18 +157,22 @@ func TestGetEnvironment(t *testing.T) {
assertEnvironment(t, environment, tests.DefaultEnvironmentIDAsInteger) assertEnvironment(t, environment, tests.DefaultEnvironmentIDAsInteger)
}) })
t.Run("Added environments can be retrieved without fetch", func(t *testing.T) { for _, useAWS := range isAWSEnvironment {
createEnvironment(t, tests.AnotherEnvironmentIDAsString) t.Run(fmt.Sprintf("AWS-%t", useAWS), func(t *testing.T) {
t.Run("Added environments can be retrieved without fetch", func(t *testing.T) {
createEnvironment(t, tests.AnotherEnvironmentIDAsString, useAWS)
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString)
response, err := http.Get(path) //nolint:gosec // because we build this path right above response, err := http.Get(path) //nolint:gosec // because we build this path right above
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode) require.Equal(t, http.StatusOK, response.StatusCode)
environment := getEnvironmentFromResponse(t, response) environment := getEnvironmentFromResponse(t, response)
assertEnvironment(t, environment, tests.AnotherEnvironmentIDAsInteger) assertEnvironment(t, environment, tests.AnotherEnvironmentIDAsInteger)
}) })
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString) deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
})
}
t.Run("Added environments can be retrieved with fetch", func(t *testing.T) { t.Run("Added environments can be retrieved with fetch", func(t *testing.T) {
// Add environment without Poseidon // Add environment without Poseidon
@ -188,17 +201,21 @@ func TestGetEnvironment(t *testing.T) {
} }
func TestDeleteEnvironment(t *testing.T) { func TestDeleteEnvironment(t *testing.T) {
t.Run("Removes added environment", func(t *testing.T) { for _, useAWS := range isAWSEnvironment {
createEnvironment(t, tests.AnotherEnvironmentIDAsString) t.Run(fmt.Sprintf("AWS-%t", useAWS), func(t *testing.T) {
t.Run("Removes added environment", func(t *testing.T) {
createEnvironment(t, tests.AnotherEnvironmentIDAsString, useAWS)
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString) path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString)
response, err := helpers.HTTPDelete(path, nil) response, err := helpers.HTTPDelete(path, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, response.StatusCode) assert.Equal(t, http.StatusNoContent, response.StatusCode)
}) })
})
}
t.Run("Removes Nomad Job", func(t *testing.T) { t.Run("Removes Nomad Job", func(t *testing.T) {
createEnvironment(t, tests.AnotherEnvironmentIDAsString) createEnvironment(t, tests.AnotherEnvironmentIDAsString, false)
// Expect created Nomad job // Expect created Nomad job
jobID := nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger) jobID := nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger)
@ -295,17 +312,23 @@ func cleanupJobsForEnvironment(t *testing.T, environmentID string) {
} }
//nolint:unparam // Because its more clear if the environment id is written in the real test //nolint:unparam // Because its more clear if the environment id is written in the real test
func createEnvironment(t *testing.T, environmentID string) { func createEnvironment(t *testing.T, environmentID string, aws bool) {
t.Helper() t.Helper()
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, environmentID) path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, environmentID)
request := dto.ExecutionEnvironmentRequest{ request := dto.ExecutionEnvironmentRequest{
PrewarmingPoolSize: 1, PrewarmingPoolSize: 1,
CPULimit: 100, CPULimit: 100,
MemoryLimit: 100, MemoryLimit: 100,
Image: *testDockerImage,
NetworkAccess: false, NetworkAccess: false,
ExposedPorts: nil, ExposedPorts: nil,
} }
if aws {
functions := strings.Fields(config.Config.AWS.Functions)
require.NotZero(t, len(functions))
request.Image = functions[0]
} else {
request.Image = *testDockerImage
}
assertPutReturnsStatusAndZeroContent(t, path, request, http.StatusCreated) assertPutReturnsStatusAndZeroContent(t, path, request, http.StatusCreated)
} }

View File

@ -16,39 +16,41 @@ import (
) )
func (s *E2ETestSuite) TestProvideRunnerRoute() { func (s *E2ETestSuite) TestProvideRunnerRoute() {
runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ for _, environmentID := range environmentIDs {
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, s.Run(environmentID.ToString(), func() {
}) runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)})
s.Require().NoError(err) s.Require().NoError(err)
reader := bytes.NewReader(runnerRequestByteString) reader := bytes.NewReader(runnerRequestByteString)
s.Run("valid request returns a runner", func() { s.Run("valid request returns a runner", func() {
resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader) resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader)
s.Require().NoError(err) s.Require().NoError(err)
s.Equal(http.StatusOK, resp.StatusCode) s.Equal(http.StatusOK, resp.StatusCode)
runnerResponse := new(dto.RunnerResponse) runnerResponse := new(dto.RunnerResponse)
err = json.NewDecoder(resp.Body).Decode(runnerResponse) err = json.NewDecoder(resp.Body).Decode(runnerResponse)
s.Require().NoError(err) s.Require().NoError(err)
s.NotEmpty(runnerResponse.ID) s.NotEmpty(runnerResponse.ID)
}) })
s.Run("invalid request returns bad request", func() { s.Run("invalid request returns bad request", func() {
resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", strings.NewReader("")) resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", strings.NewReader(""))
s.Require().NoError(err) s.Require().NoError(err)
s.Equal(http.StatusBadRequest, resp.StatusCode) s.Equal(http.StatusBadRequest, resp.StatusCode)
}) })
s.Run("requesting runner of unknown execution environment returns not found", func() { s.Run("requesting runner of unknown execution environment returns not found", func() {
runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{ runnerRequestByteString, err := json.Marshal(dto.RunnerRequest{
ExecutionEnvironmentID: tests.NonExistingIntegerID, ExecutionEnvironmentID: tests.NonExistingIntegerID,
})
s.Require().NoError(err)
reader := bytes.NewReader(runnerRequestByteString)
resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader)
s.Require().NoError(err)
s.Equal(http.StatusNotFound, resp.StatusCode)
})
}) })
s.Require().NoError(err) }
reader := bytes.NewReader(runnerRequestByteString)
resp, err := http.Post(helpers.BuildURL(api.BasePath, api.RunnersPath), "application/json", reader)
s.Require().NoError(err)
s.Equal(http.StatusNotFound, resp.StatusCode)
})
} }
// ProvideRunner creates a runner with the given RunnerRequest via an external request. // ProvideRunner creates a runner with the given RunnerRequest via an external request.
@ -77,117 +79,143 @@ func ProvideRunner(request *dto.RunnerRequest) (string, error) {
} }
func (s *E2ETestSuite) TestDeleteRunnerRoute() { func (s *E2ETestSuite) TestDeleteRunnerRoute() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ for _, environmentID := range environmentIDs {
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, s.Run(environmentID.ToString(), func() {
}) runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)})
s.NoError(err) s.NoError(err)
s.Run("Deleting the runner returns NoContent", func() { s.Run("Deleting the runner returns NoContent", func() {
resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil)
s.NoError(err) s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode) s.Equal(http.StatusNoContent, resp.StatusCode)
}) })
s.Run("Deleting it again returns NotFound", func() { s.Run("Deleting it again returns NotFound", func() {
resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil) resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID), nil)
s.NoError(err) s.NoError(err)
s.Equal(http.StatusNotFound, resp.StatusCode) s.Equal(http.StatusNotFound, resp.StatusCode)
}) })
s.Run("Deleting non-existing runner returns NotFound", func() { s.Run("Deleting non-existing runner returns NotFound", func() {
resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID), nil) resp, err := helpers.HTTPDelete(helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID), nil)
s.NoError(err) s.NoError(err)
s.Equal(http.StatusNotFound, resp.StatusCode) s.Equal(http.StatusNotFound, resp.StatusCode)
}) })
})
}
} }
//nolint:funlen // there are a lot of tests for the files route, this function can be a little longer than 100 lines ;) //nolint:funlen // there are a lot of tests for the files route, this function can be a little longer than 100 lines ;)
func (s *E2ETestSuite) TestCopyFilesRoute() { func (s *E2ETestSuite) TestCopyFilesRoute() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ for _, environmentID := range environmentIDs {
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, s.Run(environmentID.ToString(), func() {
}) runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)})
s.NoError(err) s.NoError(err)
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}}, Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}},
}) })
s.Require().NoError(err) s.Require().NoError(err)
sendCopyRequest := func(reader io.Reader) (*http.Response, error) { sendCopyRequest := func(reader io.Reader) (*http.Response, error) {
return helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), return helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath),
"application/json", reader) "application/json", reader)
}
s.Run("File copy with valid payload succeeds", func() {
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
s.Run("File content can be printed on runner", func() {
s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent)
})
})
s.Run("Files are put in correct location", func() {
relativeFilePath := "relative/file/path.txt"
relativeFileContent := "Relative file content"
absoluteFilePath := "/tmp/absolute/file/path.txt"
absoluteFileContent := "Absolute file content"
testFilePathsCopyRequestString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Copy: []dto.File{
{Path: dto.FilePath(relativeFilePath), Content: []byte(relativeFileContent)},
{Path: dto.FilePath(absoluteFilePath), Content: []byte(absoluteFileContent)},
},
})
s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(testFilePathsCopyRequestString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
s.Run("File content of file with relative path can be printed on runner", func() {
// the print command is executed in the context of the default working directory of the container
s.assertFileContent(runnerID, relativeFilePath, relativeFileContent)
})
s.Run("File content of file with absolute path can be printed on runner", func() {
s.assertFileContent(runnerID, absoluteFilePath, absoluteFileContent)
})
})
s.Run("File deletion request deletes file on runner", func() {
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Delete: []dto.FilePath{tests.DefaultFileName},
})
s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
s.Run("File content can no longer be printed", func() {
stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName)
s.Equal("", stdout)
s.Contains(stderr, "No such file or directory")
})
})
s.Run("File copy happens after file deletion", func() {
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Delete: []dto.FilePath{tests.DefaultFileName},
Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}},
})
s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
_ = resp.Body.Close()
s.Run("File content can be printed on runner", func() {
s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent)
})
})
s.Run("File copy with invalid payload returns bad request", func() {
resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath),
"text/html", strings.NewReader(""))
s.NoError(err)
s.Equal(http.StatusBadRequest, resp.StatusCode)
})
s.Run("Copying to non-existing runner returns NotFound", func() {
resp, err := helpers.HTTPPatch(
helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID, api.UpdateFileSystemPath),
"application/json", bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNotFound, resp.StatusCode)
})
})
} }
}
s.Run("File copy with valid payload succeeds", func() { func (s *E2ETestSuite) TestCopyFilesRoute_PermissionDenied() {
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) s.Run("Nomad/If one file produces permission denied error, others are still copied", func() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger,
})
s.NoError(err) s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
s.Run("File content can be printed on runner", func() {
s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent)
})
})
s.Run("Files are put in correct location", func() {
relativeFilePath := "relative/file/path.txt"
relativeFileContent := "Relative file content"
absoluteFilePath := "/tmp/absolute/file/path.txt"
absoluteFileContent := "Absolute file content"
testFilePathsCopyRequestString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Copy: []dto.File{
{Path: dto.FilePath(relativeFilePath), Content: []byte(relativeFileContent)},
{Path: dto.FilePath(absoluteFilePath), Content: []byte(absoluteFileContent)},
},
})
s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(testFilePathsCopyRequestString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
s.Run("File content of file with relative path can be printed on runner", func() {
// the print command is executed in the context of the default working directory of the container
s.assertFileContent(runnerID, relativeFilePath, relativeFileContent)
})
s.Run("File content of file with absolute path can be printed on runner", func() {
s.assertFileContent(runnerID, absoluteFilePath, absoluteFileContent)
})
})
s.Run("File deletion request deletes file on runner", func() {
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Delete: []dto.FilePath{tests.DefaultFileName},
})
s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
s.Run("File content can no longer be printed", func() {
stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName)
s.Equal("", stdout)
s.Contains(stderr, "No such file or directory")
})
})
s.Run("File copy happens after file deletion", func() {
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Delete: []dto.FilePath{tests.DefaultFileName},
Copy: []dto.File{{Path: tests.DefaultFileName, Content: []byte(tests.DefaultFileContent)}},
})
s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
_ = resp.Body.Close()
s.Run("File content can be printed on runner", func() {
s.assertFileContent(runnerID, tests.DefaultFileName, tests.DefaultFileContent)
})
})
s.Run("If one file produces permission denied error, others are still copied", func() {
newFileContent := []byte("New content") newFileContent := []byte("New content")
copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{ copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
Copy: []dto.File{ Copy: []dto.File{
@ -197,7 +225,8 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
}) })
s.Require().NoError(err) s.Require().NoError(err)
resp, err := sendCopyRequest(bytes.NewReader(copyFilesRequestByteString)) resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath),
"application/json", bytes.NewReader(copyFilesRequestByteString))
s.NoError(err) s.NoError(err)
s.Equal(http.StatusInternalServerError, resp.StatusCode) s.Equal(http.StatusInternalServerError, resp.StatusCode)
internalServerError := new(dto.InternalServerError) internalServerError := new(dto.InternalServerError)
@ -211,49 +240,70 @@ func (s *E2ETestSuite) TestCopyFilesRoute() {
}) })
}) })
s.Run("File copy with invalid payload returns bad request", func() { s.Run("AWS/If one file produces permission denied error, others are still copied", func() {
resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath), for _, environmentID := range environmentIDs {
"text/html", strings.NewReader("")) if environmentID == tests.DefaultEnvironmentIDAsInteger {
s.NoError(err) continue
s.Equal(http.StatusBadRequest, resp.StatusCode) }
}) s.Run(environmentID.ToString(), func() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)})
s.NoError(err)
s.Run("Copying to non-existing runner returns NotFound", func() { newFileContent := []byte("New content")
resp, err := helpers.HTTPPatch( copyFilesRequestByteString, err := json.Marshal(&dto.UpdateFileSystemRequest{
helpers.BuildURL(api.BasePath, api.RunnersPath, tests.NonExistingStringID, api.UpdateFileSystemPath), Copy: []dto.File{
"application/json", bytes.NewReader(copyFilesRequestByteString)) {Path: "/dev/sda", Content: []byte(tests.DefaultFileContent)},
s.NoError(err) {Path: tests.DefaultFileName, Content: newFileContent},
s.Equal(http.StatusNotFound, resp.StatusCode) },
})
s.Require().NoError(err)
resp, err := helpers.HTTPPatch(helpers.BuildURL(api.BasePath, api.RunnersPath, runnerID, api.UpdateFileSystemPath),
"application/json", bytes.NewReader(copyFilesRequestByteString))
s.NoError(err)
s.Equal(http.StatusNoContent, resp.StatusCode)
_ = resp.Body.Close()
stdout, stderr := s.PrintContentOfFileOnRunner(runnerID, tests.DefaultFileName)
s.Equal(string(newFileContent), stdout)
s.Contains(stderr, "Permission denied")
})
}
}) })
} }
func (s *E2ETestSuite) TestRunnerGetsDestroyedAfterInactivityTimeout() { func (s *E2ETestSuite) TestRunnerGetsDestroyedAfterInactivityTimeout() {
inactivityTimeout := 5 // seconds for _, environmentID := range environmentIDs {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ s.Run(environmentID.ToString(), func() {
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, inactivityTimeout := 2 // seconds
InactivityTimeout: inactivityTimeout, runnerID, err := ProvideRunner(&dto.RunnerRequest{
}) ExecutionEnvironmentID: int(environmentID),
s.Require().NoError(err) InactivityTimeout: inactivityTimeout,
})
s.Require().NoError(err)
executionTerminated := make(chan bool) executionTerminated := make(chan bool)
var lastMessage *dto.WebSocketMessage var lastMessage *dto.WebSocketMessage
go func() { go func() {
webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "sleep infinity"}) webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "sleep infinity"})
s.Require().NoError(err) s.Require().NoError(err)
connection, err := ConnectToWebSocket(webSocketURL) connection, err := ConnectToWebSocket(webSocketURL)
s.Require().NoError(err) s.Require().NoError(err)
messages, err := helpers.ReceiveAllWebSocketMessages(connection) messages, err := helpers.ReceiveAllWebSocketMessages(connection)
if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) {
s.Fail("websocket abnormal closure") s.Fail("websocket abnormal closure")
} }
controlMessages := helpers.WebSocketControlMessages(messages) controlMessages := helpers.WebSocketControlMessages(messages)
s.Require().NotEmpty(controlMessages) s.Require().NotEmpty(controlMessages)
lastMessage = controlMessages[len(controlMessages)-1] lastMessage = controlMessages[len(controlMessages)-1]
executionTerminated <- true log.Warn("")
}() executionTerminated <- true
s.Require().True(tests.ChannelReceivesSomething(executionTerminated, time.Duration(inactivityTimeout+5)*time.Second)) }()
s.Equal(dto.WebSocketMetaTimeout, lastMessage.Type) s.Require().True(tests.ChannelReceivesSomething(executionTerminated, time.Duration(inactivityTimeout+5)*time.Second))
s.Equal(dto.WebSocketMetaTimeout, lastMessage.Type)
})
}
} }
func (s *E2ETestSuite) assertFileContent(runnerID, fileName, expectedContent string) { func (s *E2ETestSuite) assertFileContent(runnerID, fileName, expectedContent string) {
@ -263,8 +313,10 @@ func (s *E2ETestSuite) assertFileContent(runnerID, fileName, expectedContent str
} }
func (s *E2ETestSuite) PrintContentOfFileOnRunner(runnerID, filename string) (stdout, stderr string) { func (s *E2ETestSuite) PrintContentOfFileOnRunner(runnerID, filename string) (stdout, stderr string) {
webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{
&dto.ExecutionRequest{Command: fmt.Sprintf("cat %s", filename)}) Command: fmt.Sprintf("cat %s", filename),
TimeLimit: int(tests.DefaultTestTimeout.Seconds()),
})
s.Require().NoError(err) s.Require().NoError(err)
connection, err := ConnectToWebSocket(webSocketURL) connection, err := ConnectToWebSocket(webSocketURL)
s.Require().NoError(err) s.Require().NoError(err)

View File

@ -18,81 +18,95 @@ import (
) )
func (s *E2ETestSuite) TestExecuteCommandRoute() { func (s *E2ETestSuite) TestExecuteCommandRoute() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ for _, environmentID := range environmentIDs {
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, s.Run(environmentID.ToString(), func() {
}) runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)})
s.Require().NoError(err) s.Require().NoError(err)
webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "true"}) webSocketURL, err := ProvideWebSocketURL(&s.Suite, runnerID, &dto.ExecutionRequest{Command: "true"})
s.Require().NoError(err) s.Require().NoError(err)
s.NotEqual("", webSocketURL) s.NotEqual("", webSocketURL)
var connection *websocket.Conn var connection *websocket.Conn
var connectionClosed bool var connectionClosed bool
connection, err = ConnectToWebSocket(webSocketURL) connection, err = ConnectToWebSocket(webSocketURL)
s.Require().NoError(err, "websocket connects") s.Require().NoError(err, "websocket connects")
closeHandler := connection.CloseHandler() closeHandler := connection.CloseHandler()
connection.SetCloseHandler(func(code int, text string) error { connection.SetCloseHandler(func(code int, text string) error {
connectionClosed = true connectionClosed = true
return closeHandler(code, text) return closeHandler(code, text)
}) })
startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) startMessage, err := helpers.ReceiveNextWebSocketMessage(connection)
s.Require().NoError(err) s.Require().NoError(err)
s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, startMessage) s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, startMessage)
exitMessage, err := helpers.ReceiveNextWebSocketMessage(connection) exitMessage, err := helpers.ReceiveNextWebSocketMessage(connection)
s.Require().NoError(err) s.Require().NoError(err)
s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, exitMessage) s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, exitMessage)
_, err = helpers.ReceiveAllWebSocketMessages(connection) _, err = helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err) s.Require().Error(err)
s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
_, _, err = connection.ReadMessage() _, _, err = connection.ReadMessage()
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure)) s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
s.True(connectionClosed, "connection should be closed") s.True(connectionClosed, "connection should be closed")
})
}
} }
func (s *E2ETestSuite) TestOutputToStdout() { func (s *E2ETestSuite) TestOutputToStdout() {
connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "echo Hello World"}) for _, environmentID := range environmentIDs {
s.Require().NoError(err) s.Run(environmentID.ToString(), func() {
connection, err := ProvideWebSocketConnection(&s.Suite, environmentID,
&dto.ExecutionRequest{Command: "echo -n Hello World"})
s.Require().NoError(err)
messages, err := helpers.ReceiveAllWebSocketMessages(connection) messages, err := helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err) s.Require().Error(err)
s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
controlMessages := helpers.WebSocketControlMessages(messages) controlMessages := helpers.WebSocketControlMessages(messages)
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0]) s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0])
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, controlMessages[1]) s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit}, controlMessages[1])
stdout, _, _ := helpers.WebSocketOutputMessages(messages) stdout, _, _ := helpers.WebSocketOutputMessages(messages)
s.Require().Equal("Hello World\r\n", stdout) s.Require().Equal("Hello World", stdout)
})
}
} }
func (s *E2ETestSuite) TestOutputToStderr() { func (s *E2ETestSuite) TestOutputToStderr() {
connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "cat -invalid"}) for _, environmentID := range environmentIDs {
s.Require().NoError(err) s.Run(environmentID.ToString(), func() {
connection, err := ProvideWebSocketConnection(&s.Suite, environmentID,
&dto.ExecutionRequest{Command: "cat -invalid"})
s.Require().NoError(err)
messages, err := helpers.ReceiveAllWebSocketMessages(connection) messages, err := helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err) s.Require().Error(err)
s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
controlMessages := helpers.WebSocketControlMessages(messages) controlMessages := helpers.WebSocketControlMessages(messages)
s.Require().Equal(2, len(controlMessages)) s.Require().Equal(2, len(controlMessages))
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0]) s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaStart}, controlMessages[0])
s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 1}, controlMessages[1]) s.Require().Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 1}, controlMessages[1])
stdout, stderr, errors := helpers.WebSocketOutputMessages(messages) stdout, stderr, errors := helpers.WebSocketOutputMessages(messages)
s.NotContains(stdout, "cat: invalid option", "Stdout should not contain the error") s.NotContains(stdout, "cat: invalid option", "Stdout should not contain the error")
s.Contains(stderr, "cat: invalid option", "Stderr should contain the error") s.Contains(stderr, "cat: invalid option", "Stderr should contain the error")
s.Empty(errors) s.Empty(errors)
})
}
} }
// AWS environments do not support stdin at this moment therefore they cannot take this test.
func (s *E2ETestSuite) TestCommandHead() { func (s *E2ETestSuite) TestCommandHead() {
hello := "Hello World!" hello := "Hello World!"
connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "head -n 1"}) connection, err := ProvideWebSocketConnection(&s.Suite, tests.DefaultEnvironmentIDAsInteger,
&dto.ExecutionRequest{Command: "head -n 1"})
s.Require().NoError(err) s.Require().NoError(err)
startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) startMessage, err := helpers.ReceiveNextWebSocketMessage(connection)
@ -110,49 +124,58 @@ func (s *E2ETestSuite) TestCommandHead() {
} }
func (s *E2ETestSuite) TestCommandReturnsAfterTimeout() { func (s *E2ETestSuite) TestCommandReturnsAfterTimeout() {
connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{Command: "sleep 4", TimeLimit: 1}) for _, environmentID := range environmentIDs {
s.Require().NoError(err) s.Run(environmentID.ToString(), func() {
connection, err := ProvideWebSocketConnection(&s.Suite, environmentID,
&dto.ExecutionRequest{Command: "sleep 4", TimeLimit: 1})
s.Require().NoError(err)
c := make(chan bool) c := make(chan bool)
var messages []*dto.WebSocketMessage var messages []*dto.WebSocketMessage
go func() { go func() {
messages, err = helpers.ReceiveAllWebSocketMessages(connection) messages, err = helpers.ReceiveAllWebSocketMessages(connection)
if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) { if !s.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err) {
s.T().Fail()
}
close(c)
}()
select {
case <-time.After(2 * time.Second):
s.T().Fatal("The execution should have returned by now")
case <-c:
if s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout}, messages[len(messages)-1]) {
return
}
}
s.T().Fail() s.T().Fail()
} })
close(c)
}()
select {
case <-time.After(2 * time.Second):
s.T().Fatal("The execution should have returned by now")
case <-c:
if s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout}, messages[len(messages)-1]) {
return
}
} }
s.T().Fail()
} }
func (s *E2ETestSuite) TestEchoEnvironment() { func (s *E2ETestSuite) TestEchoEnvironment() {
connection, err := ProvideWebSocketConnection(&s.Suite, &dto.ExecutionRequest{ for _, environmentID := range environmentIDs {
Command: "echo $hello", s.Run(environmentID.ToString(), func() {
Environment: map[string]string{"hello": "world"}, connection, err := ProvideWebSocketConnection(&s.Suite, environmentID, &dto.ExecutionRequest{
}) Command: "echo -n $hello",
s.Require().NoError(err) Environment: map[string]string{"hello": "world"},
})
s.Require().NoError(err)
startMessage, err := helpers.ReceiveNextWebSocketMessage(connection) startMessage, err := helpers.ReceiveNextWebSocketMessage(connection)
s.Require().NoError(err) s.Require().NoError(err)
s.Equal(dto.WebSocketMetaStart, startMessage.Type) s.Equal(dto.WebSocketMetaStart, startMessage.Type)
messages, err := helpers.ReceiveAllWebSocketMessages(connection) messages, err := helpers.ReceiveAllWebSocketMessages(connection)
s.Require().Error(err) s.Require().Error(err)
s.Equal(err, &websocket.CloseError{Code: websocket.CloseNormalClosure}) s.Equal(err, &websocket.CloseError{Code: websocket.CloseNormalClosure})
stdout, _, _ := helpers.WebSocketOutputMessages(messages) stdout, _, _ := helpers.WebSocketOutputMessages(messages)
s.Equal("world\r\n", stdout) s.Equal("world", stdout)
})
}
} }
func (s *E2ETestSuite) TestStderrFifoIsRemoved() { func (s *E2ETestSuite) TestNomadStderrFifoIsRemoved() {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ runnerID, err := ProvideRunner(&dto.RunnerRequest{
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger, ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger,
}) })
@ -191,11 +214,9 @@ func (s *E2ETestSuite) ListTempDirectory(runnerID string) string {
} }
// ProvideWebSocketConnection establishes a client WebSocket connection to run the passed ExecutionRequest. // ProvideWebSocketConnection establishes a client WebSocket connection to run the passed ExecutionRequest.
// It requires a running Poseidon instance. func ProvideWebSocketConnection(
func ProvideWebSocketConnection(s *suite.Suite, request *dto.ExecutionRequest) (*websocket.Conn, error) { s *suite.Suite, environmentID dto.EnvironmentID, request *dto.ExecutionRequest) (*websocket.Conn, error) {
runnerID, err := ProvideRunner(&dto.RunnerRequest{ runnerID, err := ProvideRunner(&dto.RunnerRequest{ExecutionEnvironmentID: int(environmentID)})
ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger,
})
if err != nil { if err != nil {
return nil, fmt.Errorf("error providing runner: %w", err) return nil, fmt.Errorf("error providing runner: %w", err)
} }