Implement routes to list, get and delete execution environments
* #9 Implement routes to list, get and delete execution environments. A refactoring was required to introduce the ExecutionEnvironment interface. * Fix MR comments, linting issues and bug that lead to e2e test failure * Add e2e tests * Add unit tests
This commit is contained in:
@@ -9,11 +9,16 @@ import (
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
executionEnvironmentIDKey = "executionEnvironmentId"
|
||||
fetchEnvironmentKey = "fetch"
|
||||
listRouteName = "list"
|
||||
getRouteName = "get"
|
||||
createOrUpdateRouteName = "createOrUpdate"
|
||||
deleteRouteName = "delete"
|
||||
)
|
||||
|
||||
var ErrMissingURLParameter = errors.New("url parameter missing")
|
||||
@@ -22,10 +27,82 @@ type EnvironmentController struct {
|
||||
manager environment.Manager
|
||||
}
|
||||
|
||||
type ExecutionEnvironmentsResponse struct {
|
||||
ExecutionEnvironments []runner.ExecutionEnvironment `json:"executionEnvironments"`
|
||||
}
|
||||
|
||||
func (e *EnvironmentController) ConfigureRoutes(router *mux.Router) {
|
||||
environmentRouter := router.PathPrefix(EnvironmentsPath).Subrouter()
|
||||
environmentRouter.HandleFunc("", e.list).Methods(http.MethodGet).Name(listRouteName)
|
||||
|
||||
specificEnvironmentRouter := environmentRouter.Path(fmt.Sprintf("/{%s:[0-9]+}", executionEnvironmentIDKey)).Subrouter()
|
||||
specificEnvironmentRouter.HandleFunc("", e.get).Methods(http.MethodGet).Name(getRouteName)
|
||||
specificEnvironmentRouter.HandleFunc("", e.createOrUpdate).Methods(http.MethodPut).Name(createOrUpdateRouteName)
|
||||
specificEnvironmentRouter.HandleFunc("", e.delete).Methods(http.MethodDelete).Name(deleteRouteName)
|
||||
}
|
||||
|
||||
// list returns all information about available execution environments.
|
||||
func (e *EnvironmentController) list(writer http.ResponseWriter, request *http.Request) {
|
||||
fetch, err := parseFetchParameter(request)
|
||||
if err != nil {
|
||||
writeBadRequest(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
environments, err := e.manager.List(fetch)
|
||||
if err != nil {
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(writer, ExecutionEnvironmentsResponse{environments}, http.StatusOK)
|
||||
}
|
||||
|
||||
// get returns all information about the requested execution environment.
|
||||
func (e *EnvironmentController) get(writer http.ResponseWriter, request *http.Request) {
|
||||
environmentID, err := parseEnvironmentID(request)
|
||||
if err != nil {
|
||||
// This case is never used as the router validates the id format
|
||||
writeBadRequest(writer, err)
|
||||
return
|
||||
}
|
||||
fetch, err := parseFetchParameter(request)
|
||||
if err != nil {
|
||||
writeBadRequest(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
executionEnvironment, err := e.manager.Get(environmentID, fetch)
|
||||
if errors.Is(err, runner.ErrUnknownExecutionEnvironment) {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(writer, executionEnvironment, http.StatusOK)
|
||||
}
|
||||
|
||||
// delete removes the specified execution environment.
|
||||
func (e *EnvironmentController) delete(writer http.ResponseWriter, request *http.Request) {
|
||||
environmentID, err := parseEnvironmentID(request)
|
||||
if err != nil {
|
||||
// This case is never used as the router validates the id format
|
||||
writeBadRequest(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
found, err := e.manager.Delete(environmentID)
|
||||
if err != nil {
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
return
|
||||
} else if !found {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// createOrUpdate creates/updates an execution environment on the executor.
|
||||
@@ -35,17 +112,12 @@ func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, reque
|
||||
writeBadRequest(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := mux.Vars(request)[executionEnvironmentIDKey]
|
||||
if !ok {
|
||||
writeBadRequest(writer, fmt.Errorf("could not find %s: %w", executionEnvironmentIDKey, ErrMissingURLParameter))
|
||||
return
|
||||
}
|
||||
environmentID, err := runner.NewEnvironmentID(id)
|
||||
environmentID, err := parseEnvironmentID(request)
|
||||
if err != nil {
|
||||
writeBadRequest(writer, fmt.Errorf("could not update environment: %w", err))
|
||||
writeBadRequest(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
created, err := e.manager.CreateOrUpdate(environmentID, *req)
|
||||
if err != nil {
|
||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||
@@ -57,3 +129,26 @@ func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, reque
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func parseEnvironmentID(request *http.Request) (dto.EnvironmentID, error) {
|
||||
id, ok := mux.Vars(request)[executionEnvironmentIDKey]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("could not find %s: %w", executionEnvironmentIDKey, ErrMissingURLParameter)
|
||||
}
|
||||
environmentID, err := dto.NewEnvironmentID(id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not update environment: %w", err)
|
||||
}
|
||||
return environmentID, nil
|
||||
}
|
||||
|
||||
func parseFetchParameter(request *http.Request) (fetch bool, err error) {
|
||||
fetchString := request.FormValue(fetchEnvironmentKey)
|
||||
if len(fetchString) > 0 {
|
||||
fetch, err = strconv.ParseBool(fetchString)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("could not parse fetch parameter: %w", err)
|
||||
}
|
||||
}
|
||||
return fetch, nil
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/openHPI/poseidon/internal/environment"
|
||||
"github.com/openHPI/poseidon/internal/nomad"
|
||||
"github.com/openHPI/poseidon/internal/runner"
|
||||
"github.com/openHPI/poseidon/pkg/dto"
|
||||
"github.com/openHPI/poseidon/tests"
|
||||
@@ -33,10 +34,178 @@ func (s *EnvironmentControllerTestSuite) SetupTest() {
|
||||
s.router = NewRouter(nil, s.manager)
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestList() {
|
||||
call := s.manager.On("List", mock.AnythingOfType("bool"))
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{[]runner.ExecutionEnvironment{}, nil}
|
||||
})
|
||||
path, err := s.router.Get(listRouteName).URL()
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with no Environments", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
var environmentsResponse ExecutionEnvironmentsResponse
|
||||
err = json.NewDecoder(recorder.Result().Body).Decode(&environmentsResponse)
|
||||
s.Require().NoError(err)
|
||||
_ = recorder.Result().Body.Close()
|
||||
|
||||
s.Empty(environmentsResponse.ExecutionEnvironments)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("with fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "true")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "List", true)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("with bad fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "YouDecide")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusBadRequest, recorder.Code)
|
||||
s.manager.AssertNotCalled(s.T(), "List")
|
||||
})
|
||||
|
||||
s.Run("returns multiple environments", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
firstEnvironment, err := environment.NewNomadEnvironment(
|
||||
"job \"" + nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger) + "\" {}")
|
||||
s.Require().NoError(err)
|
||||
secondEnvironment, err := environment.NewNomadEnvironment(
|
||||
"job \"" + nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger) + "\" {}")
|
||||
s.Require().NoError(err)
|
||||
call.ReturnArguments = mock.Arguments{[]runner.ExecutionEnvironment{firstEnvironment, secondEnvironment}, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
paramMap := make(map[string]interface{})
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(¶mMap)
|
||||
s.Require().NoError(err)
|
||||
environmentsInterface, ok := paramMap["executionEnvironments"]
|
||||
s.Require().True(ok)
|
||||
environments, ok := environmentsInterface.([]interface{})
|
||||
s.Require().True(ok)
|
||||
s.Equal(2, len(environments))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestGet() {
|
||||
call := s.manager.On("Get", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("bool"))
|
||||
path, err := s.router.Get(getRouteName).URL(executionEnvironmentIDKey, tests.DefaultEnvironmentIDAsString)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("with unknown environment", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{nil, runner.ErrUnknownExecutionEnvironment}
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "Get", dto.EnvironmentID(0), false)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("not found with fetch", func() {
|
||||
recorder := httptest.NewRecorder()
|
||||
query := path.Query()
|
||||
query.Set("fetch", "true")
|
||||
path.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest(http.MethodGet, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{nil, runner.ErrUnknownExecutionEnvironment}
|
||||
})
|
||||
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
s.manager.AssertCalled(s.T(), "Get", dto.EnvironmentID(0), true)
|
||||
})
|
||||
s.manager.Calls = []mock.Call{}
|
||||
|
||||
s.Run("returns environment", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
testEnvironment, err := environment.NewNomadEnvironment(
|
||||
"job \"" + nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger) + "\" {}")
|
||||
s.Require().NoError(err)
|
||||
call.ReturnArguments = mock.Arguments{testEnvironment, nil}
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
var environmentParams map[string]interface{}
|
||||
err := json.NewDecoder(recorder.Result().Body).Decode(&environmentParams)
|
||||
s.Require().NoError(err)
|
||||
idInterface, ok := environmentParams["id"]
|
||||
s.Require().True(ok)
|
||||
idFloat, ok := idInterface.(float64)
|
||||
s.Require().True(ok)
|
||||
s.Equal(tests.DefaultEnvironmentIDAsInteger, int(idFloat))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnvironmentControllerTestSuite) TestDelete() {
|
||||
call := s.manager.On("Delete", mock.AnythingOfType("dto.EnvironmentID"))
|
||||
path, err := s.router.Get(deleteRouteName).URL(executionEnvironmentIDKey, tests.DefaultEnvironmentIDAsString)
|
||||
s.Require().NoError(err)
|
||||
request, err := http.NewRequest(http.MethodDelete, path.String(), nil)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("environment not found", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{false, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNotFound, recorder.Code)
|
||||
})
|
||||
|
||||
s.Run("environment deleted", func() {
|
||||
call.Run(func(args mock.Arguments) {
|
||||
call.ReturnArguments = mock.Arguments{true, nil}
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(recorder, request)
|
||||
s.Equal(http.StatusNoContent, recorder.Code)
|
||||
})
|
||||
|
||||
s.manager.Calls = []mock.Call{}
|
||||
s.Run("with bad environment id", func() {
|
||||
_, err := s.router.Get(deleteRouteName).URL(executionEnvironmentIDKey, "MagicNonNumberID")
|
||||
s.Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
type CreateOrUpdateEnvironmentTestSuite struct {
|
||||
EnvironmentControllerTestSuite
|
||||
path string
|
||||
id runner.EnvironmentID
|
||||
id dto.EnvironmentID
|
||||
body []byte
|
||||
}
|
||||
|
||||
|
@@ -48,7 +48,7 @@ func (r *RunnerController) provide(writer http.ResponseWriter, request *http.Req
|
||||
if err := parseJSONRequestBody(writer, request, runnerRequest); err != nil {
|
||||
return
|
||||
}
|
||||
environmentID := runner.EnvironmentID(runnerRequest.ExecutionEnvironmentID)
|
||||
environmentID := dto.EnvironmentID(runnerRequest.ExecutionEnvironmentID)
|
||||
nextRunner, err := r.manager.Claim(environmentID, runnerRequest.InactivityTimeout)
|
||||
if err != nil {
|
||||
switch err {
|
||||
|
@@ -122,7 +122,7 @@ func (s *ProvideRunnerTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestValidRequestReturnsRunner() {
|
||||
s.runnerManager.On("Claim", mock.AnythingOfType("runner.EnvironmentID"),
|
||||
s.runnerManager.On("Claim", mock.AnythingOfType("dto.EnvironmentID"),
|
||||
mock.AnythingOfType("int")).Return(s.runner, nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -149,7 +149,7 @@ func (s *ProvideRunnerTestSuite) TestInvalidRequestReturnsBadRequest() {
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturnsNotFound() {
|
||||
s.runnerManager.
|
||||
On("Claim", mock.AnythingOfType("runner.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, runner.ErrUnknownExecutionEnvironment)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -158,7 +158,7 @@ func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturns
|
||||
}
|
||||
|
||||
func (s *ProvideRunnerTestSuite) TestWhenNoRunnerAvailableReturnsNomadOverload() {
|
||||
s.runnerManager.On("Claim", mock.AnythingOfType("runner.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
s.runnerManager.On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||
Return(nil, runner.ErrNoRunnersAvailable)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
|
Reference in New Issue
Block a user