Fix a lot of linting issues
After we introduced the linter we haven't really touched the old code. This commit now fixes all linting issue that exist right now.
This commit is contained in:
@ -56,7 +56,8 @@ func TestNewRouterV1WithAuthenticationEnabled(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("protected route is not accessible", func(t *testing.T) {
|
||||
// request an available API route that should be guarded by authentication (which one, in particular, does not matter here)
|
||||
// request an available API route that should be guarded by authentication.
|
||||
// (which one, in particular, does not matter here)
|
||||
request, err := http.NewRequest(http.MethodPost, "/api/v1/runners", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -20,53 +20,53 @@ type AuthenticationMiddlewareTestSuite struct {
|
||||
httpAuthenticationMiddleware http.Handler
|
||||
}
|
||||
|
||||
func (suite *AuthenticationMiddlewareTestSuite) SetupTest() {
|
||||
func (s *AuthenticationMiddlewareTestSuite) SetupTest() {
|
||||
correctAuthenticationToken = []byte(testToken)
|
||||
suite.recorder = httptest.NewRecorder()
|
||||
s.recorder = httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/test", nil)
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
suite.request = request
|
||||
suite.httpAuthenticationMiddleware = HTTPAuthenticationMiddleware(
|
||||
s.request = request
|
||||
s.httpAuthenticationMiddleware = HTTPAuthenticationMiddleware(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
}
|
||||
|
||||
func (suite *AuthenticationMiddlewareTestSuite) TearDownTest() {
|
||||
func (s *AuthenticationMiddlewareTestSuite) TearDownTest() {
|
||||
correctAuthenticationToken = []byte(nil)
|
||||
}
|
||||
|
||||
func (suite *AuthenticationMiddlewareTestSuite) TestReturns401WhenHeaderUnset() {
|
||||
suite.httpAuthenticationMiddleware.ServeHTTP(suite.recorder, suite.request)
|
||||
assert.Equal(suite.T(), http.StatusUnauthorized, suite.recorder.Code)
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestReturns401WhenHeaderUnset() {
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (suite *AuthenticationMiddlewareTestSuite) TestReturns401WhenTokenWrong() {
|
||||
suite.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
suite.httpAuthenticationMiddleware.ServeHTTP(suite.recorder, suite.request)
|
||||
assert.Equal(suite.T(), http.StatusUnauthorized, suite.recorder.Code)
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestReturns401WhenTokenWrong() {
|
||||
s.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (suite *AuthenticationMiddlewareTestSuite) TestWarnsWhenUnauthorized() {
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestWarnsWhenUnauthorized() {
|
||||
var hook *test.Hook
|
||||
logger, hook := test.NewNullLogger()
|
||||
log = logger.WithField("pkg", "api/auth")
|
||||
|
||||
suite.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
suite.httpAuthenticationMiddleware.ServeHTTP(suite.recorder, suite.request)
|
||||
s.request.Header.Set(TokenHeader, "Wr0ngT0k3n")
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusUnauthorized, suite.recorder.Code)
|
||||
assert.Equal(suite.T(), logrus.WarnLevel, hook.LastEntry().Level)
|
||||
assert.Equal(suite.T(), hook.LastEntry().Data["token"], "Wr0ngT0k3n")
|
||||
assert.Equal(s.T(), http.StatusUnauthorized, s.recorder.Code)
|
||||
assert.Equal(s.T(), logrus.WarnLevel, hook.LastEntry().Level)
|
||||
assert.Equal(s.T(), hook.LastEntry().Data["token"], "Wr0ngT0k3n")
|
||||
}
|
||||
|
||||
func (suite *AuthenticationMiddlewareTestSuite) TestPassesWhenTokenCorrect() {
|
||||
suite.request.Header.Set(TokenHeader, testToken)
|
||||
suite.httpAuthenticationMiddleware.ServeHTTP(suite.recorder, suite.request)
|
||||
func (s *AuthenticationMiddlewareTestSuite) TestPassesWhenTokenCorrect() {
|
||||
s.request.Header.Set(TokenHeader, testToken)
|
||||
s.httpAuthenticationMiddleware.ServeHTTP(s.recorder, s.request)
|
||||
|
||||
assert.Equal(suite.T(), http.StatusOK, suite.recorder.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, s.recorder.Code)
|
||||
}
|
||||
|
||||
func TestHTTPAuthenticationMiddleware(t *testing.T) {
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
// RunnerRequest is the expected json structure of the request body for the ProvideRunner function.
|
||||
type RunnerRequest struct {
|
||||
ExecutionEnvironmentId int `json:"executionEnvironmentId"`
|
||||
ExecutionEnvironmentID int `json:"executionEnvironmentId"`
|
||||
InactivityTimeout int `json:"inactivityTimeout"`
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ type ExecutionRequest struct {
|
||||
}
|
||||
|
||||
func (er *ExecutionRequest) FullCommand() []string {
|
||||
var command []string
|
||||
command := make([]string, 0)
|
||||
command = append(command, "env", "-")
|
||||
for variable, value := range er.Environment {
|
||||
command = append(command, fmt.Sprintf("%s=%s", variable, value))
|
||||
@ -31,7 +31,8 @@ func (er *ExecutionRequest) FullCommand() []string {
|
||||
return command
|
||||
}
|
||||
|
||||
// ExecutionEnvironmentRequest is the expected json structure of the request body for the create execution environment function.
|
||||
// ExecutionEnvironmentRequest is the expected json structure of the request body
|
||||
// for the create execution environment function.
|
||||
type ExecutionEnvironmentRequest struct {
|
||||
PrewarmingPoolSize uint `json:"prewarmingPoolSize"`
|
||||
CPULimit uint `json:"cpuLimit"`
|
||||
@ -43,12 +44,12 @@ type ExecutionEnvironmentRequest struct {
|
||||
|
||||
// RunnerResponse is the expected response when providing a runner.
|
||||
type RunnerResponse struct {
|
||||
Id string `json:"runnerId"`
|
||||
ID string `json:"runnerId"`
|
||||
}
|
||||
|
||||
// ExecutionResponse is the expected response when creating an execution for a runner.
|
||||
type ExecutionResponse struct {
|
||||
WebSocketUrl string `json:"websocketUrl"`
|
||||
WebSocketURL string `json:"websocketUrl"`
|
||||
}
|
||||
|
||||
// UpdateFileSystemRequest is the expected json structure of the request body for the update file system route.
|
||||
@ -102,6 +103,13 @@ const (
|
||||
WebSocketExit WebSocketMessageType = "exit"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownWebSocketMessageType = errors.New("unknown WebSocket message type")
|
||||
ErrMissingType = errors.New("type is missing")
|
||||
ErrMissingData = errors.New("data is missing")
|
||||
ErrInvalidType = errors.New("invalid type")
|
||||
)
|
||||
|
||||
// WebSocketMessage is the type for all messages send in the WebSocket to the client.
|
||||
// Depending on the MessageType the Data or ExitCode might not be included in the marshaled json message.
|
||||
type WebSocketMessage struct {
|
||||
@ -112,24 +120,29 @@ type WebSocketMessage struct {
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
// This converts the WebSocketMessage into the expected schema (see docs/websocket.schema.json).
|
||||
func (m WebSocketMessage) MarshalJSON() ([]byte, error) {
|
||||
func (m WebSocketMessage) MarshalJSON() (res []byte, err error) {
|
||||
switch m.Type {
|
||||
case WebSocketOutputStdout, WebSocketOutputStderr, WebSocketOutputError:
|
||||
return json.Marshal(struct {
|
||||
res, err = json.Marshal(struct {
|
||||
MessageType WebSocketMessageType `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}{m.Type, m.Data})
|
||||
case WebSocketMetaStart, WebSocketMetaTimeout:
|
||||
return json.Marshal(struct {
|
||||
res, err = json.Marshal(struct {
|
||||
MessageType WebSocketMessageType `json:"type"`
|
||||
}{m.Type})
|
||||
case WebSocketExit:
|
||||
return json.Marshal(struct {
|
||||
res, err = json.Marshal(struct {
|
||||
MessageType WebSocketMessageType `json:"type"`
|
||||
ExitCode uint8 `json:"data"`
|
||||
}{m.Type, m.ExitCode})
|
||||
}
|
||||
return nil, errors.New("unhandled WebSocket message type")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling WebSocketMessage: %w", err)
|
||||
} else if res == nil {
|
||||
return nil, ErrUnknownWebSocketMessageType
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
@ -138,47 +151,47 @@ func (m *WebSocketMessage) UnmarshalJSON(rawMessage []byte) error {
|
||||
messageMap := make(map[string]interface{})
|
||||
err := json.Unmarshal(rawMessage, &messageMap)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error unmarshiling raw WebSocket message: %w", err)
|
||||
}
|
||||
messageType, ok := messageMap["type"]
|
||||
if !ok {
|
||||
return errors.New("missing key type")
|
||||
return ErrMissingType
|
||||
}
|
||||
messageTypeString, ok := messageType.(string)
|
||||
if !ok {
|
||||
return errors.New("value of key type must be a string")
|
||||
return fmt.Errorf("value of key type must be a string: %w", ErrInvalidType)
|
||||
}
|
||||
switch messageType := WebSocketMessageType(messageTypeString); messageType {
|
||||
case WebSocketExit:
|
||||
data, ok := messageMap["data"]
|
||||
if !ok {
|
||||
return errors.New("missing key data")
|
||||
return ErrMissingData
|
||||
}
|
||||
// json.Unmarshal converts any number to a float64 in the massageMap, so we must first cast it to the float.
|
||||
exit, ok := data.(float64)
|
||||
if !ok {
|
||||
return errors.New("value of key data must be a number")
|
||||
return fmt.Errorf("value of key data must be a number: %w", ErrInvalidType)
|
||||
}
|
||||
if exit != float64(uint8(exit)) {
|
||||
return errors.New("value of key data must be uint8")
|
||||
return fmt.Errorf("value of key data must be uint8: %w", ErrInvalidType)
|
||||
}
|
||||
m.Type = messageType
|
||||
m.ExitCode = uint8(exit)
|
||||
case WebSocketOutputStdout, WebSocketOutputStderr, WebSocketOutputError:
|
||||
data, ok := messageMap["data"]
|
||||
if !ok {
|
||||
return errors.New("missing key data")
|
||||
return ErrMissingData
|
||||
}
|
||||
text, ok := data.(string)
|
||||
if !ok {
|
||||
return errors.New("value of key data must be a string")
|
||||
return fmt.Errorf("value of key data must be a string: %w", ErrInvalidType)
|
||||
}
|
||||
m.Type = messageType
|
||||
m.Data = text
|
||||
case WebSocketMetaStart, WebSocketMetaTimeout:
|
||||
m.Type = messageType
|
||||
default:
|
||||
return errors.New("unknown WebSocket message type")
|
||||
return ErrUnknownWebSocketMessageType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||
@ -15,6 +16,8 @@ const (
|
||||
createOrUpdateRouteName = "createOrUpdate"
|
||||
)
|
||||
|
||||
var ErrMissingURLParameter = errors.New("url parameter missing")
|
||||
|
||||
type EnvironmentController struct {
|
||||
manager environment.Manager
|
||||
}
|
||||
@ -35,7 +38,7 @@ func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, reque
|
||||
|
||||
id, ok := mux.Vars(request)[executionEnvironmentIDKey]
|
||||
if !ok {
|
||||
writeBadRequest(writer, fmt.Errorf("could not find %s", executionEnvironmentIDKey))
|
||||
writeBadRequest(writer, fmt.Errorf("could not find %s: %w", executionEnvironmentIDKey, ErrMissingURLParameter))
|
||||
return
|
||||
}
|
||||
environmentID, err := runner.NewEnvironmentID(id)
|
||||
@ -54,8 +57,3 @@ func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, reque
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// delete removes an execution environment from the executor
|
||||
func (e *EnvironmentController) delete(writer http.ResponseWriter, request *http.Request) { // nolint:unused ToDo
|
||||
|
||||
}
|
||||
|
@ -2,23 +2,24 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func writeInternalServerError(writer http.ResponseWriter, err error, errorCode dto.ErrorCode) {
|
||||
sendJson(writer, &dto.InternalServerError{Message: err.Error(), ErrorCode: errorCode}, http.StatusInternalServerError)
|
||||
sendJSON(writer, &dto.InternalServerError{Message: err.Error(), ErrorCode: errorCode}, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func writeBadRequest(writer http.ResponseWriter, err error) {
|
||||
sendJson(writer, &dto.ClientError{Message: err.Error()}, http.StatusBadRequest)
|
||||
sendJSON(writer, &dto.ClientError{Message: err.Error()}, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func writeNotFound(writer http.ResponseWriter, err error) {
|
||||
sendJson(writer, &dto.ClientError{Message: err.Error()}, http.StatusNotFound)
|
||||
sendJSON(writer, &dto.ClientError{Message: err.Error()}, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func sendJson(writer http.ResponseWriter, content interface{}, httpStatusCode int) {
|
||||
func sendJSON(writer http.ResponseWriter, content interface{}, httpStatusCode int) {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(httpStatusCode)
|
||||
response, err := json.Marshal(content)
|
||||
@ -36,7 +37,7 @@ func sendJson(writer http.ResponseWriter, content interface{}, httpStatusCode in
|
||||
func parseJSONRequestBody(writer http.ResponseWriter, request *http.Request, structure interface{}) error {
|
||||
if err := json.NewDecoder(request.Body).Decode(structure); err != nil {
|
||||
writeBadRequest(writer, err)
|
||||
return err
|
||||
return fmt.Errorf("error parsing JSON request body: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
@ -16,8 +17,8 @@ const (
|
||||
WebsocketPath = "/websocket"
|
||||
UpdateFileSystemPath = "/files"
|
||||
DeleteRoute = "deleteRunner"
|
||||
RunnerIdKey = "runnerId"
|
||||
ExecutionIdKey = "executionId"
|
||||
RunnerIDKey = "runnerId"
|
||||
ExecutionIDKey = "executionID"
|
||||
ProvideRoute = "provideRunner"
|
||||
)
|
||||
|
||||
@ -30,9 +31,10 @@ type RunnerController struct {
|
||||
func (r *RunnerController) ConfigureRoutes(router *mux.Router) {
|
||||
runnersRouter := router.PathPrefix(RunnersPath).Subrouter()
|
||||
runnersRouter.HandleFunc("", r.provide).Methods(http.MethodPost).Name(ProvideRoute)
|
||||
r.runnerRouter = runnersRouter.PathPrefix(fmt.Sprintf("/{%s}", RunnerIdKey)).Subrouter()
|
||||
r.runnerRouter = runnersRouter.PathPrefix(fmt.Sprintf("/{%s}", RunnerIDKey)).Subrouter()
|
||||
r.runnerRouter.Use(r.findRunnerMiddleware)
|
||||
r.runnerRouter.HandleFunc(UpdateFileSystemPath, r.updateFileSystem).Methods(http.MethodPatch).Name(UpdateFileSystemPath)
|
||||
r.runnerRouter.HandleFunc(UpdateFileSystemPath, r.updateFileSystem).Methods(http.MethodPatch).
|
||||
Name(UpdateFileSystemPath)
|
||||
r.runnerRouter.HandleFunc(ExecutePath, r.execute).Methods(http.MethodPost).Name(ExecutePath)
|
||||
r.runnerRouter.HandleFunc(WebsocketPath, r.connectToRunner).Methods(http.MethodGet).Name(WebsocketPath)
|
||||
r.runnerRouter.HandleFunc("", r.delete).Methods(http.MethodDelete).Name(DeleteRoute)
|
||||
@ -46,21 +48,21 @@ func (r *RunnerController) provide(writer http.ResponseWriter, request *http.Req
|
||||
if err := parseJSONRequestBody(writer, request, runnerRequest); err != nil {
|
||||
return
|
||||
}
|
||||
environmentId := runner.EnvironmentID(runnerRequest.ExecutionEnvironmentId)
|
||||
nextRunner, err := r.manager.Claim(environmentId, runnerRequest.InactivityTimeout)
|
||||
environmentID := runner.EnvironmentID(runnerRequest.ExecutionEnvironmentID)
|
||||
nextRunner, err := r.manager.Claim(environmentID, runnerRequest.InactivityTimeout)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case runner.ErrUnknownExecutionEnvironment:
|
||||
writeNotFound(writer, err)
|
||||
case runner.ErrNoRunnersAvailable:
|
||||
log.WithField("environment", environmentId).Warn("No runners available")
|
||||
log.WithField("environment", environmentID).Warn("No runners available")
|
||||
writeInternalServerError(writer, err, dto.ErrorNomadOverload)
|
||||
default:
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
}
|
||||
return
|
||||
}
|
||||
sendJson(writer, &dto.RunnerResponse{Id: nextRunner.Id()}, http.StatusOK)
|
||||
sendJSON(writer, &dto.RunnerResponse{ID: nextRunner.ID()}, http.StatusOK)
|
||||
}
|
||||
|
||||
// updateFileSystem handles the files API route.
|
||||
@ -98,36 +100,36 @@ func (r *RunnerController) execute(writer http.ResponseWriter, request *http.Req
|
||||
}
|
||||
targetRunner, _ := runner.FromContext(request.Context())
|
||||
|
||||
path, err := r.runnerRouter.Get(WebsocketPath).URL(RunnerIdKey, targetRunner.Id())
|
||||
path, err := r.runnerRouter.Get(WebsocketPath).URL(RunnerIDKey, targetRunner.ID())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not create runner websocket URL.")
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
return
|
||||
}
|
||||
newUuid, err := uuid.NewRandom()
|
||||
newUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Could not create execution id")
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
return
|
||||
}
|
||||
id := runner.ExecutionId(newUuid.String())
|
||||
id := runner.ExecutionID(newUUID.String())
|
||||
targetRunner.Add(id, executionRequest)
|
||||
webSocketUrl := url.URL{
|
||||
webSocketURL := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: request.Host,
|
||||
Path: path.String(),
|
||||
RawQuery: fmt.Sprintf("%s=%s", ExecutionIdKey, id),
|
||||
RawQuery: fmt.Sprintf("%s=%s", ExecutionIDKey, id),
|
||||
}
|
||||
|
||||
sendJson(writer, &dto.ExecutionResponse{WebSocketUrl: webSocketUrl.String()}, http.StatusOK)
|
||||
sendJSON(writer, &dto.ExecutionResponse{WebSocketURL: webSocketURL.String()}, http.StatusOK)
|
||||
}
|
||||
|
||||
// The findRunnerMiddleware looks up the runnerId for routes containing it
|
||||
// and adds the runner to the context of the request.
|
||||
func (r *RunnerController) findRunnerMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
runnerId := mux.Vars(request)[RunnerIdKey]
|
||||
targetRunner, err := r.manager.Get(runnerId)
|
||||
runnerID := mux.Vars(request)[RunnerIDKey]
|
||||
targetRunner, err := r.manager.Get(runnerID)
|
||||
if err != nil {
|
||||
writeNotFound(writer, err)
|
||||
return
|
||||
@ -145,7 +147,7 @@ func (r *RunnerController) delete(writer http.ResponseWriter, request *http.Requ
|
||||
|
||||
err := r.manager.Return(targetRunner)
|
||||
if err != nil {
|
||||
if err == runner.ErrUnknownExecutionEnvironment {
|
||||
if errors.Is(err, runner.ErrUnknownExecutionEnvironment) {
|
||||
writeNotFound(writer, err)
|
||||
} else {
|
||||
writeInternalServerError(writer, err, dto.ErrorNomadInternalServerError)
|
||||
|
@ -3,7 +3,6 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@ -32,7 +31,7 @@ func (s *MiddlewareTestSuite) SetupTest() {
|
||||
s.runner = runner.NewNomadJob(tests.DefaultRunnerID, nil, nil)
|
||||
s.capturedRunner = nil
|
||||
s.runnerRequest = func(runnerId string) *http.Request {
|
||||
path, err := s.router.Get("test-runner-id").URL(RunnerIdKey, runnerId)
|
||||
path, err := s.router.Get("test-runner-id").URL(RunnerIDKey, runnerId)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPost, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
@ -50,7 +49,7 @@ func (s *MiddlewareTestSuite) SetupTest() {
|
||||
s.router = mux.NewRouter()
|
||||
runnerController := &RunnerController{s.manager, s.router}
|
||||
s.router.Use(runnerController.findRunnerMiddleware)
|
||||
s.router.HandleFunc(fmt.Sprintf("/test/{%s}", RunnerIdKey), runnerRouteHandler).Name("test-runner-id")
|
||||
s.router.HandleFunc(fmt.Sprintf("/test/{%s}", RunnerIDKey), runnerRouteHandler).Name("test-runner-id")
|
||||
}
|
||||
|
||||
func TestMiddlewareTestSuite(t *testing.T) {
|
||||
@ -58,10 +57,10 @@ func TestMiddlewareTestSuite(t *testing.T) {
|
||||
}
|
||||
|
||||
func (s *MiddlewareTestSuite) TestFindRunnerMiddlewareIfRunnerExists() {
|
||||
s.manager.On("Get", s.runner.Id()).Return(s.runner, nil)
|
||||
s.manager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, s.runnerRequest(s.runner.Id()))
|
||||
s.router.ServeHTTP(recorder, s.runnerRequest(s.runner.ID()))
|
||||
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
s.Equal(s.runner, s.capturedRunner)
|
||||
@ -86,16 +85,16 @@ type RunnerRouteTestSuite struct {
|
||||
runnerManager *runner.ManagerMock
|
||||
router *mux.Router
|
||||
runner runner.Runner
|
||||
executionId runner.ExecutionId
|
||||
executionID runner.ExecutionID
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) SetupTest() {
|
||||
s.runnerManager = &runner.ManagerMock{}
|
||||
s.router = NewRouter(s.runnerManager, nil)
|
||||
s.runner = runner.NewNomadJob("some-id", nil, nil)
|
||||
s.executionId = "execution-id"
|
||||
s.runner.Add(s.executionId, &dto.ExecutionRequest{})
|
||||
s.runnerManager.On("Get", s.runner.Id()).Return(s.runner, nil)
|
||||
s.executionID = "execution-id"
|
||||
s.runner.Add(s.executionID, &dto.ExecutionRequest{})
|
||||
s.runnerManager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
}
|
||||
|
||||
func TestProvideRunnerTestSuite(t *testing.T) {
|
||||
@ -115,7 +114,7 @@ func (s *ProvideRunnerTestSuite) SetupTest() {
|
||||
s.Require().NoError(err)
|
||||
s.path = path.String()
|
||||
|
||||
runnerRequest := dto.RunnerRequest{ExecutionEnvironmentId: tests.DefaultEnvironmentIDAsInteger}
|
||||
runnerRequest := dto.RunnerRequest{ExecutionEnvironmentID: tests.DefaultEnvironmentIDAsInteger}
|
||||
body, err := json.Marshal(runnerRequest)
|
||||
s.Require().NoError(err)
|
||||
s.defaultRequest, err = http.NewRequest(http.MethodPost, s.path, bytes.NewReader(body))
|
||||
@ -123,7 +122,8 @@ func (s *ProvideRunnerTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestValidRequestReturnsRunner() {
|
||||
s.runnerManager.On("Claim", mock.AnythingOfType("runner.EnvironmentID"), mock.AnythingOfType("int")).Return(s.runner, nil)
|
||||
s.runnerManager.On("Claim", mock.AnythingOfType("runner.EnvironmentID"),
|
||||
mock.AnythingOfType("int")).Return(s.runner, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
s.router.ServeHTTP(recorder, s.defaultRequest)
|
||||
@ -134,7 +134,7 @@ func (s *ProvideRunnerTestSuite) TestValidRequestReturnsRunner() {
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&runnerResponse)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
s.Equal(s.runner.Id(), runnerResponse.Id)
|
||||
s.Equal(s.runner.ID(), runnerResponse.ID)
|
||||
})
|
||||
}
|
||||
|
||||
@ -173,7 +173,7 @@ func (s *ProvideRunnerTestSuite) TestWhenNoRunnerAvailableReturnsNomadOverload()
|
||||
}
|
||||
|
||||
func (s *RunnerRouteTestSuite) TestExecuteRoute() {
|
||||
path, err := s.router.Get(ExecutePath).URL(RunnerIdKey, s.runner.Id())
|
||||
path, err := s.router.Get(ExecutePath).URL(RunnerIDKey, s.runner.ID())
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("valid request", func() {
|
||||
@ -197,12 +197,12 @@ func (s *RunnerRouteTestSuite) TestExecuteRoute() {
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
s.Run("creates an execution request for the runner", func() {
|
||||
webSocketUrl, err := url.Parse(webSocketResponse.WebSocketUrl)
|
||||
webSocketURL, err := url.Parse(webSocketResponse.WebSocketURL)
|
||||
s.Require().NoError(err)
|
||||
executionId := webSocketUrl.Query().Get(ExecutionIdKey)
|
||||
storedExecutionRequest, ok := s.runner.Pop(runner.ExecutionId(executionId))
|
||||
executionID := webSocketURL.Query().Get(ExecutionIDKey)
|
||||
storedExecutionRequest, ok := s.runner.Pop(runner.ExecutionID(executionID))
|
||||
|
||||
s.True(ok, "No execution request with this id: ", executionId)
|
||||
s.True(ok, "No execution request with this id: ", executionID)
|
||||
s.Equal(&executionRequest, storedExecutionRequest)
|
||||
})
|
||||
})
|
||||
@ -231,9 +231,9 @@ type UpdateFileSystemRouteTestSuite struct {
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
routeUrl, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIdKey, tests.DefaultMockID)
|
||||
routeURL, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, tests.DefaultMockID)
|
||||
s.Require().NoError(err)
|
||||
s.path = routeUrl.String()
|
||||
s.path = routeURL.String()
|
||||
s.runnerMock = &runner.RunnerMock{}
|
||||
s.runnerManager.On("Get", tests.DefaultMockID).Return(s.runnerMock, nil)
|
||||
s.recorder = httptest.NewRecorder()
|
||||
@ -243,8 +243,10 @@ func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsNoContentOnV
|
||||
s.runnerMock.On("UpdateFileSystem", mock.AnythingOfType("*dto.UpdateFileSystemRequest")).Return(nil)
|
||||
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, _ := json.Marshal(copyRequest)
|
||||
request, _ := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusNoContent, s.recorder.Code)
|
||||
@ -252,7 +254,8 @@ func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsNoContentOnV
|
||||
}
|
||||
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsBadRequestOnInvalidRequestBody() {
|
||||
request, _ := http.NewRequest(http.MethodPatch, s.path, strings.NewReader(""))
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, strings.NewReader(""))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusBadRequest, s.recorder.Code)
|
||||
@ -261,10 +264,13 @@ func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsBadRequestOn
|
||||
func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemToNonExistingRunnerReturnsNotFound() {
|
||||
invalidID := "some-invalid-runner-id"
|
||||
s.runnerManager.On("Get", invalidID).Return(nil, runner.ErrRunnerNotFound)
|
||||
path, _ := s.router.Get(UpdateFileSystemPath).URL(RunnerIdKey, invalidID)
|
||||
path, err := s.router.Get(UpdateFileSystemPath).URL(RunnerIDKey, invalidID)
|
||||
s.Require().NoError(err)
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, _ := json.Marshal(copyRequest)
|
||||
request, _ := http.NewRequest(http.MethodPatch, path.String(), bytes.NewReader(body))
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, path.String(), bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusNotFound, s.recorder.Code)
|
||||
@ -276,8 +282,10 @@ func (s *UpdateFileSystemRouteTestSuite) TestUpdateFileSystemReturnsInternalServ
|
||||
Return(runner.ErrorFileCopyFailed)
|
||||
|
||||
copyRequest := dto.UpdateFileSystemRequest{}
|
||||
body, _ := json.Marshal(copyRequest)
|
||||
request, _ := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
body, err := json.Marshal(copyRequest)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodPatch, s.path, bytes.NewReader(body))
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(s.recorder, request)
|
||||
s.Equal(http.StatusInternalServerError, s.recorder.Code)
|
||||
@ -294,7 +302,7 @@ type DeleteRunnerRouteTestSuite struct {
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) SetupTest() {
|
||||
s.RunnerRouteTestSuite.SetupTest()
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIdKey, s.runner.Id())
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIDKey, s.runner.ID())
|
||||
s.Require().NoError(err)
|
||||
s.path = deleteURL.String()
|
||||
}
|
||||
@ -316,7 +324,7 @@ func (s *DeleteRunnerRouteTestSuite) TestValidRequestReturnsNoContent() {
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestReturnInternalServerErrorWhenApiCallToNomadFailed() {
|
||||
s.runnerManager.On("Return", s.runner).Return(errors.New("API call failed"))
|
||||
s.runnerManager.On("Return", s.runner).Return(tests.ErrDefault)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest(http.MethodDelete, s.path, nil)
|
||||
@ -328,8 +336,8 @@ func (s *DeleteRunnerRouteTestSuite) TestReturnInternalServerErrorWhenApiCallToN
|
||||
}
|
||||
|
||||
func (s *DeleteRunnerRouteTestSuite) TestDeleteInvalidRunnerIdReturnsNotFound() {
|
||||
s.runnerManager.On("Get", mock.AnythingOfType("string")).Return(nil, errors.New("API call failed"))
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIdKey, "1nv4l1dID")
|
||||
s.runnerManager.On("Get", mock.AnythingOfType("string")).Return(nil, tests.ErrDefault)
|
||||
deleteURL, err := s.router.Get(DeleteRoute).URL(RunnerIDKey, "1nv4l1dID")
|
||||
s.Require().NoError(err)
|
||||
deletePath := deleteURL.String()
|
||||
|
||||
|
134
api/websocket.go
134
api/websocket.go
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/runner"
|
||||
@ -11,6 +12,10 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const CodeOceanToRawReaderBufferSize = 1024
|
||||
|
||||
var ErrUnknownExecutionID = errors.New("execution id unknown")
|
||||
|
||||
type webSocketConnection interface {
|
||||
WriteMessage(messageType int, data []byte) error
|
||||
Close() error
|
||||
@ -21,11 +26,11 @@ type webSocketConnection interface {
|
||||
|
||||
type WebSocketReader interface {
|
||||
io.Reader
|
||||
readInputLoop() context.CancelFunc
|
||||
startReadInputLoop() context.CancelFunc
|
||||
}
|
||||
|
||||
// codeOceanToRawReader is an io.Reader implementation that provides the content of the WebSocket connection to CodeOcean.
|
||||
// You have to start the Reader by calling readInputLoop. After that you can use the Read function.
|
||||
// codeOceanToRawReader is an io.Reader implementation that provides the content of the WebSocket connection
|
||||
// to CodeOcean. You have to start the Reader by calling readInputLoop. After that you can use the Read function.
|
||||
type codeOceanToRawReader struct {
|
||||
connection webSocketConnection
|
||||
|
||||
@ -38,74 +43,79 @@ type codeOceanToRawReader struct {
|
||||
func newCodeOceanToRawReader(connection webSocketConnection) *codeOceanToRawReader {
|
||||
return &codeOceanToRawReader{
|
||||
connection: connection,
|
||||
buffer: make(chan byte, 1024),
|
||||
buffer: make(chan byte, CodeOceanToRawReaderBufferSize),
|
||||
}
|
||||
}
|
||||
|
||||
// readInputLoop asynchronously reads from the WebSocket connection and buffers the user's input.
|
||||
// readInputLoop reads from the WebSocket connection and buffers the user's input.
|
||||
// This is necessary because input must be read for the connection to handle special messages like close and call the
|
||||
// CloseHandler.
|
||||
func (cr *codeOceanToRawReader) readInputLoop() context.CancelFunc {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
readMessage := make(chan bool)
|
||||
for {
|
||||
var messageType int
|
||||
var reader io.Reader
|
||||
var err error
|
||||
func (cr *codeOceanToRawReader) readInputLoop(ctx context.Context) {
|
||||
readMessage := make(chan bool)
|
||||
for {
|
||||
var messageType int
|
||||
var reader io.Reader
|
||||
var err error
|
||||
|
||||
go func() {
|
||||
messageType, reader, err = cr.connection.NextReader()
|
||||
readMessage <- true
|
||||
}()
|
||||
go func() {
|
||||
messageType, reader, err = cr.connection.NextReader()
|
||||
readMessage <- true
|
||||
}()
|
||||
select {
|
||||
case <-readMessage:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Error reading client message")
|
||||
return
|
||||
}
|
||||
if messageType != websocket.TextMessage {
|
||||
log.WithField("messageType", messageType).Warn("Received message of wrong type")
|
||||
return
|
||||
}
|
||||
|
||||
message, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("error while reading WebSocket message")
|
||||
return
|
||||
}
|
||||
for _, character := range message {
|
||||
select {
|
||||
case <-readMessage:
|
||||
case cr.buffer <- character:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Error reading client message")
|
||||
return
|
||||
}
|
||||
if messageType != websocket.TextMessage {
|
||||
log.WithField("messageType", messageType).Warn("Received message of wrong type")
|
||||
return
|
||||
}
|
||||
|
||||
message, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("error while reading WebSocket message")
|
||||
return
|
||||
}
|
||||
for _, character := range message {
|
||||
select {
|
||||
case cr.buffer <- character:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// startReadInputLoop start the read input loop asynchronously and returns a context.CancelFunc which can be used
|
||||
// to cancel the read input loop.
|
||||
func (cr *codeOceanToRawReader) startReadInputLoop() context.CancelFunc {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go cr.readInputLoop(ctx)
|
||||
return cancel
|
||||
}
|
||||
|
||||
// Read implements the io.Reader interface.
|
||||
// It returns bytes from the buffer.
|
||||
func (cr *codeOceanToRawReader) Read(p []byte) (n int, err error) {
|
||||
func (cr *codeOceanToRawReader) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return
|
||||
return 0, nil
|
||||
}
|
||||
// Ensure to not return until at least one byte has been read to avoid busy waiting.
|
||||
p[0] = <-cr.buffer
|
||||
var n int
|
||||
for n = 1; n < len(p); n++ {
|
||||
select {
|
||||
case p[n] = <-cr.buffer:
|
||||
default:
|
||||
return
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// rawToCodeOceanWriter is an io.Writer implementation that, when written to, wraps the written data in the appropriate
|
||||
@ -137,7 +147,7 @@ func upgradeConnection(writer http.ResponseWriter, request *http.Request) (webSo
|
||||
connection, err := connUpgrader.Upgrade(writer, request, nil)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Connection upgrade failed")
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("error upgrading the connection: %w", err)
|
||||
}
|
||||
return connection, nil
|
||||
}
|
||||
@ -161,7 +171,7 @@ func newWebSocketProxy(connection webSocketConnection) (*webSocketProxy, error)
|
||||
|
||||
closeHandler := connection.CloseHandler()
|
||||
connection.SetCloseHandler(func(code int, text string) error {
|
||||
// The default close handler always returns nil, so the error can be safely ignored.
|
||||
//nolint:errcheck // The default close handler always returns nil, so the error can be safely ignored.
|
||||
_ = closeHandler(code, text)
|
||||
close(proxy.userExit)
|
||||
return nil
|
||||
@ -173,7 +183,7 @@ func newWebSocketProxy(connection webSocketConnection) (*webSocketProxy, error)
|
||||
// and handles WebSocket exit messages.
|
||||
func (wp *webSocketProxy) waitForExit(exit <-chan runner.ExitInfo, cancelExecution context.CancelFunc) {
|
||||
defer wp.close()
|
||||
cancelInputLoop := wp.Stdin.readInputLoop()
|
||||
cancelInputLoop := wp.Stdin.startReadInputLoop()
|
||||
var exitInfo runner.ExitInfo
|
||||
select {
|
||||
case exitInfo = <-exit:
|
||||
@ -187,12 +197,18 @@ func (wp *webSocketProxy) waitForExit(exit <-chan runner.ExitInfo, cancelExecuti
|
||||
}
|
||||
|
||||
if errors.Is(exitInfo.Err, context.DeadlineExceeded) || errors.Is(exitInfo.Err, runner.ErrorRunnerInactivityTimeout) {
|
||||
_ = wp.sendToClient(dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout})
|
||||
err := wp.sendToClient(dto.WebSocketMessage{Type: dto.WebSocketMetaTimeout})
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Failed to send timeout message to client")
|
||||
}
|
||||
return
|
||||
} else if exitInfo.Err != nil {
|
||||
errorMessage := "Error executing the request"
|
||||
log.WithError(exitInfo.Err).Warn(errorMessage)
|
||||
_ = wp.sendToClient(dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: errorMessage})
|
||||
err := wp.sendToClient(dto.WebSocketMessage{Type: dto.WebSocketOutputError, Data: errorMessage})
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Failed to send output error message to client")
|
||||
}
|
||||
return
|
||||
}
|
||||
log.WithField("exit_code", exitInfo.Code).Debug()
|
||||
@ -211,27 +227,29 @@ func (wp *webSocketProxy) sendToClient(message dto.WebSocketMessage) error {
|
||||
if err != nil {
|
||||
log.WithField("message", message).WithError(err).Warn("Marshal error")
|
||||
wp.closeWithError("Error creating message")
|
||||
return err
|
||||
return fmt.Errorf("error marshaling WebSocket message: %w", err)
|
||||
}
|
||||
err = wp.connection.WriteMessage(websocket.TextMessage, encodedMessage)
|
||||
if err != nil {
|
||||
errorMessage := "Error writing the exit message"
|
||||
log.WithError(err).Warn(errorMessage)
|
||||
wp.closeWithError(errorMessage)
|
||||
return err
|
||||
return fmt.Errorf("error writing WebSocket message: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wp *webSocketProxy) closeWithError(message string) {
|
||||
err := wp.connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, message))
|
||||
err := wp.connection.WriteMessage(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseInternalServerErr, message))
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Error during websocket close")
|
||||
}
|
||||
}
|
||||
|
||||
func (wp *webSocketProxy) close() {
|
||||
err := wp.connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
err := wp.connection.WriteMessage(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
_ = wp.connection.Close()
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Error during websocket close")
|
||||
@ -241,10 +259,10 @@ func (wp *webSocketProxy) close() {
|
||||
// connectToRunner is the endpoint for websocket connections.
|
||||
func (r *RunnerController) connectToRunner(writer http.ResponseWriter, request *http.Request) {
|
||||
targetRunner, _ := runner.FromContext(request.Context())
|
||||
executionId := runner.ExecutionId(request.URL.Query().Get(ExecutionIdKey))
|
||||
executionRequest, ok := targetRunner.Pop(executionId)
|
||||
executionID := runner.ExecutionID(request.URL.Query().Get(ExecutionIDKey))
|
||||
executionRequest, ok := targetRunner.Pop(executionID)
|
||||
if !ok {
|
||||
writeNotFound(writer, errors.New("executionId does not exist"))
|
||||
writeNotFound(writer, ErrUnknownExecutionID)
|
||||
return
|
||||
}
|
||||
|
||||
@ -258,7 +276,7 @@ func (r *RunnerController) connectToRunner(writer http.ResponseWriter, request *
|
||||
return
|
||||
}
|
||||
|
||||
log.WithField("runnerId", targetRunner.Id()).WithField("executionId", executionId).Info("Running execution")
|
||||
log.WithField("runnerId", targetRunner.ID()).WithField("executionID", executionID).Info("Running execution")
|
||||
exit, cancel := targetRunner.ExecuteInteractively(executionRequest, proxy.Stdin, proxy.Stdout, proxy.Stderr)
|
||||
|
||||
proxy.waitForExit(exit, cancel)
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
@ -34,144 +33,146 @@ func TestWebSocketTestSuite(t *testing.T) {
|
||||
type WebSocketTestSuite struct {
|
||||
suite.Suite
|
||||
router *mux.Router
|
||||
executionId runner.ExecutionId
|
||||
executionID runner.ExecutionID
|
||||
runner runner.Runner
|
||||
apiMock *nomad.ExecutorAPIMock
|
||||
server *httptest.Server
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) SetupTest() {
|
||||
runnerId := "runner-id"
|
||||
suite.runner, suite.apiMock = newNomadAllocationWithMockedApiClient(runnerId)
|
||||
func (s *WebSocketTestSuite) SetupTest() {
|
||||
runnerID := "runner-id"
|
||||
s.runner, s.apiMock = newNomadAllocationWithMockedAPIClient(runnerID)
|
||||
|
||||
// default execution
|
||||
suite.executionId = "execution-id"
|
||||
suite.runner.Add(suite.executionId, &executionRequestHead)
|
||||
mockApiExecuteHead(suite.apiMock)
|
||||
s.executionID = "execution-id"
|
||||
s.runner.Add(s.executionID, &executionRequestHead)
|
||||
mockAPIExecuteHead(s.apiMock)
|
||||
|
||||
runnerManager := &runner.ManagerMock{}
|
||||
runnerManager.On("Get", suite.runner.Id()).Return(suite.runner, nil)
|
||||
suite.router = NewRouter(runnerManager, nil)
|
||||
suite.server = httptest.NewServer(suite.router)
|
||||
runnerManager.On("Get", s.runner.ID()).Return(s.runner, nil)
|
||||
s.router = NewRouter(runnerManager, nil)
|
||||
s.server = httptest.NewServer(s.router)
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TearDownTest() {
|
||||
suite.server.Close()
|
||||
func (s *WebSocketTestSuite) TearDownTest() {
|
||||
s.server.Close()
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketConnectionCanBeEstablished() {
|
||||
wsUrl, err := suite.webSocketUrl("ws", suite.runner.Id(), suite.executionId)
|
||||
suite.Require().NoError(err)
|
||||
_, _, err = websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
func (s *WebSocketTestSuite) TestWebsocketConnectionCanBeEstablished() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
_, _, err = websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketReturns404IfExecutionDoesNotExist() {
|
||||
wsUrl, err := suite.webSocketUrl("ws", suite.runner.Id(), "invalid-execution-id")
|
||||
suite.Require().NoError(err)
|
||||
_, response, _ := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Equal(http.StatusNotFound, response.StatusCode)
|
||||
func (s *WebSocketTestSuite) TestWebsocketReturns404IfExecutionDoesNotExist() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), "invalid-execution-id")
|
||||
s.Require().NoError(err)
|
||||
_, response, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().ErrorIs(err, websocket.ErrBadHandshake)
|
||||
s.Equal(http.StatusNotFound, response.StatusCode)
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketReturns400IfRequestedViaHttp() {
|
||||
wsUrl, err := suite.webSocketUrl("http", suite.runner.Id(), suite.executionId)
|
||||
suite.Require().NoError(err)
|
||||
response, err := http.Get(wsUrl.String())
|
||||
suite.Require().NoError(err)
|
||||
func (s *WebSocketTestSuite) TestWebsocketReturns400IfRequestedViaHttp() {
|
||||
wsURL, err := s.webSocketURL("http", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
response, err := http.Get(wsURL.String())
|
||||
s.Require().NoError(err)
|
||||
// This functionality is implemented by the WebSocket library.
|
||||
suite.Equal(http.StatusBadRequest, response.StatusCode)
|
||||
s.Equal(http.StatusBadRequest, response.StatusCode)
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketConnection() {
|
||||
wsUrl, err := suite.webSocketUrl("ws", suite.runner.Id(), suite.executionId)
|
||||
suite.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
func (s *WebSocketTestSuite) TestWebsocketConnection() {
|
||||
wsURL, err := s.webSocketURL("ws", s.runner.ID(), s.executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
err = connection.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
suite.Require().NoError(err)
|
||||
s.Require().NoError(err)
|
||||
|
||||
suite.Run("Receives start message", func() {
|
||||
s.Run("Receives start message", func() {
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
})
|
||||
|
||||
suite.Run("Executes the request in the runner", func() {
|
||||
s.Run("Executes the request in the runner", func() {
|
||||
<-time.After(100 * time.Millisecond)
|
||||
suite.apiMock.AssertCalled(suite.T(), "ExecuteCommand",
|
||||
s.apiMock.AssertCalled(s.T(), "ExecuteCommand",
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
suite.Run("Can send input", func() {
|
||||
s.Run("Can send input", func() {
|
||||
err = connection.WriteMessage(websocket.TextMessage, []byte("Hello World\n"))
|
||||
suite.Require().NoError(err)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
suite.Require().Error(err)
|
||||
suite.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
suite.Run("Receives output message", func() {
|
||||
s.Run("Receives output message", func() {
|
||||
stdout, _, _ := helpers.WebSocketOutputMessages(messages)
|
||||
suite.Equal("Hello World", stdout)
|
||||
s.Equal("Hello World", stdout)
|
||||
})
|
||||
|
||||
suite.Run("Receives exit message", func() {
|
||||
s.Run("Receives exit message", func() {
|
||||
controlMessages := helpers.WebSocketControlMessages(messages)
|
||||
suite.Require().Equal(1, len(controlMessages))
|
||||
suite.Equal(dto.WebSocketExit, controlMessages[0].Type)
|
||||
s.Require().Equal(1, len(controlMessages))
|
||||
s.Equal(dto.WebSocketExit, controlMessages[0].Type)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestCancelWebSocketConnection() {
|
||||
executionId := runner.ExecutionId("sleeping-execution")
|
||||
suite.runner.Add(executionId, &executionRequestSleep)
|
||||
canceled := mockApiExecuteSleep(suite.apiMock)
|
||||
func (s *WebSocketTestSuite) TestCancelWebSocketConnection() {
|
||||
executionID := runner.ExecutionID("sleeping-execution")
|
||||
s.runner.Add(executionID, &executionRequestSleep)
|
||||
canceled := mockAPIExecuteSleep(s.apiMock)
|
||||
|
||||
wsUrl, err := webSocketUrl("ws", suite.server, suite.router, suite.runner.Id(), executionId)
|
||||
suite.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
suite.Fail("ExecuteInteractively canceled unexpected")
|
||||
s.Fail("ExecuteInteractively canceled unexpected")
|
||||
default:
|
||||
}
|
||||
|
||||
err = connection.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second))
|
||||
suite.Require().NoError(err)
|
||||
err = connection.WriteControl(websocket.CloseMessage,
|
||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second))
|
||||
s.Require().NoError(err)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
suite.Fail("ExecuteInteractively not canceled")
|
||||
s.Fail("ExecuteInteractively not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebSocketConnectionTimeout() {
|
||||
executionId := runner.ExecutionId("time-out-execution")
|
||||
func (s *WebSocketTestSuite) TestWebSocketConnectionTimeout() {
|
||||
executionID := runner.ExecutionID("time-out-execution")
|
||||
limitExecution := executionRequestSleep
|
||||
limitExecution.TimeLimit = 2
|
||||
suite.runner.Add(executionId, &limitExecution)
|
||||
canceled := mockApiExecuteSleep(suite.apiMock)
|
||||
s.runner.Add(executionID, &limitExecution)
|
||||
canceled := mockAPIExecuteSleep(s.apiMock)
|
||||
|
||||
wsUrl, err := webSocketUrl("ws", suite.server, suite.router, suite.runner.Id(), executionId)
|
||||
suite.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaStart, message.Type)
|
||||
|
||||
select {
|
||||
case <-canceled:
|
||||
suite.Fail("ExecuteInteractively canceled unexpected")
|
||||
s.Fail("ExecuteInteractively canceled unexpected")
|
||||
case <-time.After(time.Duration(limitExecution.TimeLimit-1) * time.Second):
|
||||
<-time.After(time.Second)
|
||||
}
|
||||
@ -179,94 +180,94 @@ func (suite *WebSocketTestSuite) TestWebSocketConnectionTimeout() {
|
||||
select {
|
||||
case <-canceled:
|
||||
case <-time.After(time.Second):
|
||||
suite.Fail("ExecuteInteractively not canceled")
|
||||
s.Fail("ExecuteInteractively not canceled")
|
||||
}
|
||||
|
||||
message, err = helpers.ReceiveNextWebSocketMessage(connection)
|
||||
suite.Require().NoError(err)
|
||||
suite.Equal(dto.WebSocketMetaTimeout, message.Type)
|
||||
s.Require().NoError(err)
|
||||
s.Equal(dto.WebSocketMetaTimeout, message.Type)
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketStdoutAndStderr() {
|
||||
executionId := runner.ExecutionId("ls-execution")
|
||||
suite.runner.Add(executionId, &executionRequestLs)
|
||||
mockApiExecuteLs(suite.apiMock)
|
||||
func (s *WebSocketTestSuite) TestWebsocketStdoutAndStderr() {
|
||||
executionID := runner.ExecutionID("ls-execution")
|
||||
s.runner.Add(executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(s.apiMock)
|
||||
|
||||
wsUrl, err := webSocketUrl("ws", suite.server, suite.router, suite.runner.Id(), executionId)
|
||||
suite.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
suite.Require().Error(err)
|
||||
suite.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
stdout, stderr, _ := helpers.WebSocketOutputMessages(messages)
|
||||
|
||||
suite.Contains(stdout, "existing-file")
|
||||
s.Contains(stdout, "existing-file")
|
||||
|
||||
suite.Contains(stderr, "non-existing-file")
|
||||
s.Contains(stderr, "non-existing-file")
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketError() {
|
||||
executionId := runner.ExecutionId("error-execution")
|
||||
suite.runner.Add(executionId, &executionRequestError)
|
||||
mockApiExecuteError(suite.apiMock)
|
||||
func (s *WebSocketTestSuite) TestWebsocketError() {
|
||||
executionID := runner.ExecutionID("error-execution")
|
||||
s.runner.Add(executionID, &executionRequestError)
|
||||
mockAPIExecuteError(s.apiMock)
|
||||
|
||||
wsUrl, err := webSocketUrl("ws", suite.server, suite.router, suite.runner.Id(), executionId)
|
||||
suite.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
suite.Require().Error(err)
|
||||
suite.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
_, _, errMessages := helpers.WebSocketOutputMessages(messages)
|
||||
suite.Equal(1, len(errMessages))
|
||||
suite.Equal("Error executing the request", errMessages[0])
|
||||
s.Equal(1, len(errMessages))
|
||||
s.Equal("Error executing the request", errMessages[0])
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) TestWebsocketNonZeroExit() {
|
||||
executionId := runner.ExecutionId("exit-execution")
|
||||
suite.runner.Add(executionId, &executionRequestExitNonZero)
|
||||
mockApiExecuteExitNonZero(suite.apiMock)
|
||||
func (s *WebSocketTestSuite) TestWebsocketNonZeroExit() {
|
||||
executionID := runner.ExecutionID("exit-execution")
|
||||
s.runner.Add(executionID, &executionRequestExitNonZero)
|
||||
mockAPIExecuteExitNonZero(s.apiMock)
|
||||
|
||||
wsUrl, err := webSocketUrl("ws", suite.server, suite.router, suite.runner.Id(), executionId)
|
||||
suite.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsUrl.String(), nil)
|
||||
suite.Require().NoError(err)
|
||||
wsURL, err := webSocketURL("ws", s.server, s.router, s.runner.ID(), executionID)
|
||||
s.Require().NoError(err)
|
||||
connection, _, err := websocket.DefaultDialer.Dial(wsURL.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
messages, err := helpers.ReceiveAllWebSocketMessages(connection)
|
||||
suite.Require().Error(err)
|
||||
suite.Equal(&websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
|
||||
s.Require().Error(err)
|
||||
s.True(websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
|
||||
controlMessages := helpers.WebSocketControlMessages(messages)
|
||||
suite.Equal(2, len(controlMessages))
|
||||
suite.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 42}, controlMessages[1])
|
||||
s.Equal(2, len(controlMessages))
|
||||
s.Equal(&dto.WebSocketMessage{Type: dto.WebSocketExit, ExitCode: 42}, controlMessages[1])
|
||||
}
|
||||
|
||||
func TestWebsocketTLS(t *testing.T) {
|
||||
runnerId := "runner-id"
|
||||
r, apiMock := newNomadAllocationWithMockedApiClient(runnerId)
|
||||
runnerID := "runner-id"
|
||||
r, apiMock := newNomadAllocationWithMockedAPIClient(runnerID)
|
||||
|
||||
executionId := runner.ExecutionId("execution-id")
|
||||
r.Add(executionId, &executionRequestLs)
|
||||
mockApiExecuteLs(apiMock)
|
||||
executionID := runner.ExecutionID("execution-id")
|
||||
r.Add(executionID, &executionRequestLs)
|
||||
mockAPIExecuteLs(apiMock)
|
||||
|
||||
runnerManager := &runner.ManagerMock{}
|
||||
runnerManager.On("Get", r.Id()).Return(r, nil)
|
||||
runnerManager.On("Get", r.ID()).Return(r, nil)
|
||||
router := NewRouter(runnerManager, nil)
|
||||
|
||||
server, err := helpers.StartTLSServer(t, router)
|
||||
require.NoError(t, err)
|
||||
defer server.Close()
|
||||
|
||||
wsUrl, err := webSocketUrl("wss", server, router, runnerId, executionId)
|
||||
wsURL, err := webSocketURL("wss", server, router, runnerID, executionID)
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &tls.Config{RootCAs: nil, InsecureSkipVerify: true}
|
||||
config := &tls.Config{RootCAs: nil, InsecureSkipVerify: true} //nolint:gosec // test needs self-signed cert
|
||||
d := websocket.Dialer{TLSClientConfig: config}
|
||||
connection, _, err := d.Dial(wsUrl.String(), nil)
|
||||
connection, _, err := d.Dial(wsURL.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
message, err := helpers.ReceiveNextWebSocketMessage(connection)
|
||||
@ -274,7 +275,7 @@ func TestWebsocketTLS(t *testing.T) {
|
||||
assert.Equal(t, dto.WebSocketMetaStart, message.Type)
|
||||
_, err = helpers.ReceiveAllWebSocketMessages(connection)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, &websocket.CloseError{Code: websocket.CloseNormalClosure}, err)
|
||||
assert.True(t, websocket.IsCloseError(err, websocket.CloseNormalClosure))
|
||||
}
|
||||
|
||||
func TestRawToCodeOceanWriter(t *testing.T) {
|
||||
@ -284,7 +285,9 @@ func TestRawToCodeOceanWriter(t *testing.T) {
|
||||
connectionMock := &webSocketConnectionMock{}
|
||||
connectionMock.On("WriteMessage", mock.AnythingOfType("int"), mock.AnythingOfType("[]uint8")).
|
||||
Run(func(args mock.Arguments) {
|
||||
message = args.Get(1).([]byte)
|
||||
var ok bool
|
||||
message, ok = args.Get(1).([]byte)
|
||||
require.True(t, ok)
|
||||
}).
|
||||
Return(nil)
|
||||
connectionMock.On("CloseHandler").Return(nil)
|
||||
@ -300,10 +303,11 @@ func TestRawToCodeOceanWriter(t *testing.T) {
|
||||
_, err = writer.Write([]byte(testMessage))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := json.Marshal(struct {
|
||||
expected, err := json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}{string(dto.WebSocketOutputStdout), testMessage})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected, message)
|
||||
}
|
||||
|
||||
@ -312,8 +316,10 @@ func TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasRead(t *testing.T) {
|
||||
|
||||
read := make(chan bool)
|
||||
go func() {
|
||||
//nolint:makezero // we can't make zero initial length here as the reader otherwise doesn't block
|
||||
p := make([]byte, 10)
|
||||
_, _ = reader.Read(p)
|
||||
_, err := reader.Read(p)
|
||||
require.NoError(t, err)
|
||||
read <- true
|
||||
}()
|
||||
|
||||
@ -340,13 +346,15 @@ func TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasReadFromConnection(t *tes
|
||||
})
|
||||
|
||||
reader := newCodeOceanToRawReader(connection)
|
||||
cancel := reader.readInputLoop()
|
||||
cancel := reader.startReadInputLoop()
|
||||
defer cancel()
|
||||
|
||||
read := make(chan bool)
|
||||
//nolint:makezero // this is required here to make the Read call blocking
|
||||
message := make([]byte, 10)
|
||||
go func() {
|
||||
_, _ = reader.Read(message)
|
||||
_, err := reader.Read(message)
|
||||
require.NoError(t, err)
|
||||
read <- true
|
||||
}()
|
||||
|
||||
@ -362,36 +370,39 @@ func TestCodeOceanToRawReaderReturnsOnlyAfterOneByteWasReadFromConnection(t *tes
|
||||
|
||||
// --- Test suite specific test helpers ---
|
||||
|
||||
func newNomadAllocationWithMockedApiClient(runnerId string) (r runner.Runner, mock *nomad.ExecutorAPIMock) {
|
||||
mock = &nomad.ExecutorAPIMock{}
|
||||
r = runner.NewNomadJob(runnerId, mock, nil)
|
||||
func newNomadAllocationWithMockedAPIClient(runnerID string) (r runner.Runner, executorAPIMock *nomad.ExecutorAPIMock) {
|
||||
executorAPIMock = &nomad.ExecutorAPIMock{}
|
||||
r = runner.NewNomadJob(runnerID, executorAPIMock, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func webSocketUrl(scheme string, server *httptest.Server, router *mux.Router, runnerId string, executionId runner.ExecutionId) (*url.URL, error) {
|
||||
websocketUrl, err := url.Parse(server.URL)
|
||||
func webSocketURL(scheme string, server *httptest.Server, router *mux.Router,
|
||||
runnerID string, executionID runner.ExecutionID,
|
||||
) (*url.URL, error) {
|
||||
websocketURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := router.Get(WebsocketPath).URL(RunnerIdKey, runnerId)
|
||||
path, err := router.Get(WebsocketPath).URL(RunnerIDKey, runnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websocketUrl.Scheme = scheme
|
||||
websocketUrl.Path = path.Path
|
||||
websocketUrl.RawQuery = fmt.Sprintf("executionId=%s", executionId)
|
||||
return websocketUrl, nil
|
||||
websocketURL.Scheme = scheme
|
||||
websocketURL.Path = path.Path
|
||||
websocketURL.RawQuery = fmt.Sprintf("executionID=%s", executionID)
|
||||
return websocketURL, nil
|
||||
}
|
||||
|
||||
func (suite *WebSocketTestSuite) webSocketUrl(scheme, runnerId string, executionId runner.ExecutionId) (*url.URL, error) {
|
||||
return webSocketUrl(scheme, suite.server, suite.router, runnerId, executionId)
|
||||
func (s *WebSocketTestSuite) webSocketURL(scheme, runnerID string, executionID runner.ExecutionID) (*url.URL, error) {
|
||||
return webSocketURL(scheme, s.server, s.router, runnerID, executionID)
|
||||
}
|
||||
|
||||
var executionRequestLs = dto.ExecutionRequest{Command: "ls"}
|
||||
|
||||
// mockApiExecuteLs mocks the ExecuteCommand of an ExecutorApi to act as if 'ls existing-file non-existing-file' was executed.
|
||||
func mockApiExecuteLs(api *nomad.ExecutorAPIMock) {
|
||||
mockApiExecute(api, &executionRequestLs,
|
||||
// mockAPIExecuteLs mocks the ExecuteCommand of an ExecutorApi to act as if
|
||||
// 'ls existing-file non-existing-file' was executed.
|
||||
func mockAPIExecuteLs(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestLs,
|
||||
func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, stdout, stderr io.Writer) (int, error) {
|
||||
_, _ = stdout.Write([]byte("existing-file\n"))
|
||||
_, _ = stderr.Write([]byte("ls: cannot access 'non-existing-file': No such file or directory\n"))
|
||||
@ -401,10 +412,12 @@ func mockApiExecuteLs(api *nomad.ExecutorAPIMock) {
|
||||
|
||||
var executionRequestHead = dto.ExecutionRequest{Command: "head -n 1"}
|
||||
|
||||
// mockApiExecuteHead mocks the ExecuteCommand of an ExecutorApi to act as if 'head -n 1' was executed.
|
||||
func mockApiExecuteHead(api *nomad.ExecutorAPIMock) {
|
||||
mockApiExecute(api, &executionRequestHead,
|
||||
func(_ string, _ context.Context, _ []string, _ bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
||||
// mockAPIExecuteHead mocks the ExecuteCommand of an ExecutorApi to act as if 'head -n 1' was executed.
|
||||
func mockAPIExecuteHead(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestHead,
|
||||
func(_ string, _ context.Context, _ []string, _ bool,
|
||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||
) (int, error) {
|
||||
scanner := bufio.NewScanner(stdin)
|
||||
for !scanner.Scan() {
|
||||
scanner = bufio.NewScanner(stdin)
|
||||
@ -416,11 +429,13 @@ func mockApiExecuteHead(api *nomad.ExecutorAPIMock) {
|
||||
|
||||
var executionRequestSleep = dto.ExecutionRequest{Command: "sleep infinity"}
|
||||
|
||||
// mockApiExecuteSleep mocks the ExecuteCommand method of an ExecutorAPI to sleep until the execution is canceled.
|
||||
func mockApiExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
||||
// mockAPIExecuteSleep mocks the ExecuteCommand method of an ExecutorAPI to sleep until the execution is canceled.
|
||||
func mockAPIExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
||||
canceled := make(chan bool, 1)
|
||||
mockApiExecute(api, &executionRequestSleep,
|
||||
func(_ string, ctx context.Context, _ []string, _ bool, stdin io.Reader, stdout io.Writer, stderr io.Writer) (int, error) {
|
||||
mockAPIExecute(api, &executionRequestSleep,
|
||||
func(_ string, ctx context.Context, _ []string, _ bool,
|
||||
stdin io.Reader, stdout io.Writer, stderr io.Writer,
|
||||
) (int, error) {
|
||||
<-ctx.Done()
|
||||
close(canceled)
|
||||
return 0, ctx.Err()
|
||||
@ -430,28 +445,30 @@ func mockApiExecuteSleep(api *nomad.ExecutorAPIMock) <-chan bool {
|
||||
|
||||
var executionRequestError = dto.ExecutionRequest{Command: "error"}
|
||||
|
||||
// mockApiExecuteError mocks the ExecuteCommand method of an ExecutorApi to return an error.
|
||||
func mockApiExecuteError(api *nomad.ExecutorAPIMock) {
|
||||
mockApiExecute(api, &executionRequestError,
|
||||
// mockAPIExecuteError mocks the ExecuteCommand method of an ExecutorApi to return an error.
|
||||
func mockAPIExecuteError(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestError,
|
||||
func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 0, errors.New("intended error")
|
||||
return 0, tests.ErrDefault
|
||||
})
|
||||
}
|
||||
|
||||
var executionRequestExitNonZero = dto.ExecutionRequest{Command: "exit 42"}
|
||||
|
||||
// mockApiExecuteExitNonZero mocks the ExecuteCommand method of an ExecutorApi to exit with exit status 42.
|
||||
func mockApiExecuteExitNonZero(api *nomad.ExecutorAPIMock) {
|
||||
mockApiExecute(api, &executionRequestExitNonZero,
|
||||
// mockAPIExecuteExitNonZero mocks the ExecuteCommand method of an ExecutorApi to exit with exit status 42.
|
||||
func mockAPIExecuteExitNonZero(api *nomad.ExecutorAPIMock) {
|
||||
mockAPIExecute(api, &executionRequestExitNonZero,
|
||||
func(_ string, _ context.Context, _ []string, _ bool, _ io.Reader, _, _ io.Writer) (int, error) {
|
||||
return 42, nil
|
||||
})
|
||||
}
|
||||
|
||||
// mockApiExecute mocks the ExecuteCommand method of an ExecutorApi to call the given method run when the command
|
||||
// mockAPIExecute mocks the ExecuteCommand method of an ExecutorApi to call the given method run when the command
|
||||
// corresponding to the given ExecutionRequest is called.
|
||||
func mockApiExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
||||
run func(runnerId string, ctx context.Context, command []string, tty bool, stdin io.Reader, stdout, stderr io.Writer) (int, error)) {
|
||||
func mockAPIExecute(api *nomad.ExecutorAPIMock, request *dto.ExecutionRequest,
|
||||
run func(runnerId string, ctx context.Context, command []string, tty bool,
|
||||
stdin io.Reader, stdout, stderr io.Writer) (int, error),
|
||||
) {
|
||||
call := api.On("ExecuteCommand",
|
||||
mock.AnythingOfType("string"),
|
||||
mock.Anything,
|
||||
|
Reference in New Issue
Block a user