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:
@ -321,6 +321,14 @@ paths:
|
|||||||
description: List all execution environments the API is aware of.
|
description: List all execution environments the API is aware of.
|
||||||
tags:
|
tags:
|
||||||
- execution environment
|
- execution environment
|
||||||
|
parameters:
|
||||||
|
- name: fetch
|
||||||
|
in: query
|
||||||
|
description: Specify whether environments should be fetched again from the executor before returning. Otherwise, the data currently in cache is returned.
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success. Returns all execution environments
|
description: Success. Returns all execution environments
|
||||||
@ -350,6 +358,14 @@ paths:
|
|||||||
description: Get a representation of the execution environment specified by the id.
|
description: Get a representation of the execution environment specified by the id.
|
||||||
tags:
|
tags:
|
||||||
- execution environment
|
- execution environment
|
||||||
|
parameters:
|
||||||
|
- name: fetch
|
||||||
|
in: query
|
||||||
|
description: Specify whether the environment should be fetched again from the executor before returning. Otherwise, the data currently in cache is returned.
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success. Returns the execution environment
|
description: Success. Returns the execution environment
|
||||||
|
@ -9,11 +9,16 @@ import (
|
|||||||
"github.com/openHPI/poseidon/internal/runner"
|
"github.com/openHPI/poseidon/internal/runner"
|
||||||
"github.com/openHPI/poseidon/pkg/dto"
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
executionEnvironmentIDKey = "executionEnvironmentId"
|
executionEnvironmentIDKey = "executionEnvironmentId"
|
||||||
|
fetchEnvironmentKey = "fetch"
|
||||||
|
listRouteName = "list"
|
||||||
|
getRouteName = "get"
|
||||||
createOrUpdateRouteName = "createOrUpdate"
|
createOrUpdateRouteName = "createOrUpdate"
|
||||||
|
deleteRouteName = "delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrMissingURLParameter = errors.New("url parameter missing")
|
var ErrMissingURLParameter = errors.New("url parameter missing")
|
||||||
@ -22,10 +27,82 @@ type EnvironmentController struct {
|
|||||||
manager environment.Manager
|
manager environment.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecutionEnvironmentsResponse struct {
|
||||||
|
ExecutionEnvironments []runner.ExecutionEnvironment `json:"executionEnvironments"`
|
||||||
|
}
|
||||||
|
|
||||||
func (e *EnvironmentController) ConfigureRoutes(router *mux.Router) {
|
func (e *EnvironmentController) ConfigureRoutes(router *mux.Router) {
|
||||||
environmentRouter := router.PathPrefix(EnvironmentsPath).Subrouter()
|
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 := 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.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.
|
// createOrUpdate creates/updates an execution environment on the executor.
|
||||||
@ -35,17 +112,12 @@ func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, reque
|
|||||||
writeBadRequest(writer, err)
|
writeBadRequest(writer, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
environmentID, err := parseEnvironmentID(request)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeBadRequest(writer, fmt.Errorf("could not update environment: %w", err))
|
writeBadRequest(writer, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
created, err := e.manager.CreateOrUpdate(environmentID, *req)
|
created, err := e.manager.CreateOrUpdate(environmentID, *req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
writeInternalServerError(writer, err, dto.ErrorUnknown)
|
||||||
@ -57,3 +129,26 @@ func (e *EnvironmentController) createOrUpdate(writer http.ResponseWriter, reque
|
|||||||
writer.WriteHeader(http.StatusNoContent)
|
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"
|
"encoding/json"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/openHPI/poseidon/internal/environment"
|
"github.com/openHPI/poseidon/internal/environment"
|
||||||
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
"github.com/openHPI/poseidon/internal/runner"
|
"github.com/openHPI/poseidon/internal/runner"
|
||||||
"github.com/openHPI/poseidon/pkg/dto"
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"github.com/openHPI/poseidon/tests"
|
"github.com/openHPI/poseidon/tests"
|
||||||
@ -33,10 +34,178 @@ func (s *EnvironmentControllerTestSuite) SetupTest() {
|
|||||||
s.router = NewRouter(nil, s.manager)
|
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 {
|
type CreateOrUpdateEnvironmentTestSuite struct {
|
||||||
EnvironmentControllerTestSuite
|
EnvironmentControllerTestSuite
|
||||||
path string
|
path string
|
||||||
id runner.EnvironmentID
|
id dto.EnvironmentID
|
||||||
body []byte
|
body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ func (r *RunnerController) provide(writer http.ResponseWriter, request *http.Req
|
|||||||
if err := parseJSONRequestBody(writer, request, runnerRequest); err != nil {
|
if err := parseJSONRequestBody(writer, request, runnerRequest); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
environmentID := runner.EnvironmentID(runnerRequest.ExecutionEnvironmentID)
|
environmentID := dto.EnvironmentID(runnerRequest.ExecutionEnvironmentID)
|
||||||
nextRunner, err := r.manager.Claim(environmentID, runnerRequest.InactivityTimeout)
|
nextRunner, err := r.manager.Claim(environmentID, runnerRequest.InactivityTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
|
@ -122,7 +122,7 @@ func (s *ProvideRunnerTestSuite) SetupTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProvideRunnerTestSuite) TestValidRequestReturnsRunner() {
|
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)
|
mock.AnythingOfType("int")).Return(s.runner, nil)
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ func (s *ProvideRunnerTestSuite) TestInvalidRequestReturnsBadRequest() {
|
|||||||
|
|
||||||
func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturnsNotFound() {
|
func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturnsNotFound() {
|
||||||
s.runnerManager.
|
s.runnerManager.
|
||||||
On("Claim", mock.AnythingOfType("runner.EnvironmentID"), mock.AnythingOfType("int")).
|
On("Claim", mock.AnythingOfType("dto.EnvironmentID"), mock.AnythingOfType("int")).
|
||||||
Return(nil, runner.ErrUnknownExecutionEnvironment)
|
Return(nil, runner.ErrUnknownExecutionEnvironment)
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ func (s *ProvideRunnerTestSuite) TestWhenExecutionEnvironmentDoesNotExistReturns
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProvideRunnerTestSuite) TestWhenNoRunnerAvailableReturnsNomadOverload() {
|
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)
|
Return(nil, runner.ErrNoRunnersAvailable)
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
352
internal/environment/environment.go
Normal file
352
internal/environment/environment.go
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
package environment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
|
"github.com/hashicorp/nomad/jobspec2"
|
||||||
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
|
"github.com/openHPI/poseidon/internal/runner"
|
||||||
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
portNumberBase = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorUpdatingExecutionEnvironment = errors.New("errors occurred when updating environment")
|
||||||
|
)
|
||||||
|
|
||||||
|
type NomadEnvironment struct {
|
||||||
|
jobHCL string
|
||||||
|
job *nomadApi.Job
|
||||||
|
idleRunners runner.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNomadEnvironment(jobHCL string) (*NomadEnvironment, error) {
|
||||||
|
job, err := parseJob(jobHCL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing Nomad job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NomadEnvironment{jobHCL, job, runner.NewLocalRunnerStorage()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) ID() dto.EnvironmentID {
|
||||||
|
id, err := nomad.EnvironmentIDFromTemplateJobID(*n.job.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Environment ID can not be parsed from Job")
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetID(id dto.EnvironmentID) {
|
||||||
|
name := nomad.TemplateJobID(id)
|
||||||
|
n.job.ID = &name
|
||||||
|
n.job.Name = &name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) PrewarmingPoolSize() uint {
|
||||||
|
configTaskGroup := nomad.FindOrCreateConfigTaskGroup(n.job)
|
||||||
|
count, err := strconv.Atoi(configTaskGroup.Meta[nomad.ConfigMetaPoolSizeKey])
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Prewarming pool size can not be parsed from Job")
|
||||||
|
}
|
||||||
|
return uint(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetPrewarmingPoolSize(count uint) {
|
||||||
|
taskGroup := nomad.FindOrCreateConfigTaskGroup(n.job)
|
||||||
|
|
||||||
|
if taskGroup.Meta == nil {
|
||||||
|
taskGroup.Meta = make(map[string]string)
|
||||||
|
}
|
||||||
|
taskGroup.Meta[nomad.ConfigMetaPoolSizeKey] = strconv.Itoa(int(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) CPULimit() uint {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
return uint(*defaultTask.Resources.CPU)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetCPULimit(limit uint) {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
|
||||||
|
integerCPULimit := int(limit)
|
||||||
|
defaultTask.Resources.CPU = &integerCPULimit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) MemoryLimit() uint {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
return uint(*defaultTask.Resources.MemoryMB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetMemoryLimit(limit uint) {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
|
||||||
|
integerMemoryLimit := int(limit)
|
||||||
|
defaultTask.Resources.MemoryMB = &integerMemoryLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) Image() string {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
image, ok := defaultTask.Config["image"].(string)
|
||||||
|
if !ok {
|
||||||
|
image = ""
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetImage(image string) {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
defaultTask.Config["image"] = image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) NetworkAccess() (allowed bool, ports []uint16) {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
|
||||||
|
allowed = defaultTask.Config["network_mode"] != "none"
|
||||||
|
if len(defaultTaskGroup.Networks) > 0 {
|
||||||
|
networkResource := defaultTaskGroup.Networks[0]
|
||||||
|
for _, port := range networkResource.DynamicPorts {
|
||||||
|
ports = append(ports, uint16(port.To))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowed, ports
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetNetworkAccess(allow bool, exposedPorts []uint16) {
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(n.job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
|
||||||
|
if len(defaultTaskGroup.Tasks) == 0 {
|
||||||
|
// This function is only used internally and must be called as last step when configuring the task.
|
||||||
|
// This error is not recoverable.
|
||||||
|
log.Fatal("Can't configure network before task has been configured!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if allow {
|
||||||
|
var networkResource *nomadApi.NetworkResource
|
||||||
|
if len(defaultTaskGroup.Networks) == 0 {
|
||||||
|
networkResource = &nomadApi.NetworkResource{}
|
||||||
|
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{networkResource}
|
||||||
|
} else {
|
||||||
|
networkResource = defaultTaskGroup.Networks[0]
|
||||||
|
}
|
||||||
|
// Prefer "bridge" network over "host" to have an isolated network namespace with bridged interface
|
||||||
|
// instead of joining the host network namespace.
|
||||||
|
networkResource.Mode = "bridge"
|
||||||
|
for _, portNumber := range exposedPorts {
|
||||||
|
port := nomadApi.Port{
|
||||||
|
Label: strconv.FormatUint(uint64(portNumber), portNumberBase),
|
||||||
|
To: int(portNumber),
|
||||||
|
}
|
||||||
|
networkResource.DynamicPorts = append(networkResource.DynamicPorts, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly set mode to override existing settings when updating job from without to with network.
|
||||||
|
// Don't use bridge as it collides with the bridge mode above. This results in Docker using 'bridge'
|
||||||
|
// mode, meaning all allocations will be attached to the `docker0` adapter and could reach other
|
||||||
|
// non-Nomad containers attached to it. This is avoided when using Nomads bridge network mode.
|
||||||
|
defaultTask.Config["network_mode"] = ""
|
||||||
|
} else {
|
||||||
|
// Somehow, we can't set the network mode to none in the NetworkResource on task group level.
|
||||||
|
// See https://github.com/hashicorp/nomad/issues/10540
|
||||||
|
defaultTask.Config["network_mode"] = "none"
|
||||||
|
// Explicitly set Networks to signal Nomad to remove the possibly existing networkResource
|
||||||
|
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register creates a Nomad job based on the default job configuration and the given parameters.
|
||||||
|
// It registers the job with Nomad and waits until the registration completes.
|
||||||
|
func (n *NomadEnvironment) Register(apiClient nomad.ExecutorAPI) error {
|
||||||
|
evalID, err := apiClient.RegisterNomadJob(n.job)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't register job: %w", err)
|
||||||
|
}
|
||||||
|
err = apiClient.MonitorEvaluation(evalID, context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error during the monitoring of the environment job: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) Delete(apiClient nomad.ExecutorAPI) error {
|
||||||
|
err := n.removeRunners(apiClient, uint(n.idleRunners.Length()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = apiClient.DeleteJob(*n.job.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't delete environment job: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) Scale(apiClient nomad.ExecutorAPI) error {
|
||||||
|
required := int(n.PrewarmingPoolSize()) - n.idleRunners.Length()
|
||||||
|
|
||||||
|
if required > 0 {
|
||||||
|
return n.createRunners(apiClient, uint(required))
|
||||||
|
} else {
|
||||||
|
return n.removeRunners(apiClient, uint(-required))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) UpdateRunnerSpecs(apiClient nomad.ExecutorAPI) error {
|
||||||
|
runners, err := apiClient.LoadRunnerIDs(n.ID().ToString())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update environment couldn't load runners: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var occurredError error
|
||||||
|
for _, id := range runners {
|
||||||
|
// avoid taking the address of the loop variable
|
||||||
|
runnerID := id
|
||||||
|
updatedRunnerJob := n.DeepCopyJob()
|
||||||
|
updatedRunnerJob.ID = &runnerID
|
||||||
|
updatedRunnerJob.Name = &runnerID
|
||||||
|
|
||||||
|
err := apiClient.RegisterRunnerJob(updatedRunnerJob)
|
||||||
|
if err != nil {
|
||||||
|
if occurredError == nil {
|
||||||
|
occurredError = ErrorUpdatingExecutionEnvironment
|
||||||
|
}
|
||||||
|
occurredError = fmt.Errorf("%w; new api error for runner %s - %v", occurredError, id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return occurredError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) Sample(apiClient nomad.ExecutorAPI) (runner.Runner, bool) {
|
||||||
|
r, ok := n.idleRunners.Sample()
|
||||||
|
if ok {
|
||||||
|
err := n.createRunner(apiClient)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).WithField("environmentID", n.ID()).Error("Couldn't create new runner for claimed one")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) AddRunner(r runner.Runner) {
|
||||||
|
n.idleRunners.Add(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) DeleteRunner(id string) {
|
||||||
|
n.idleRunners.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
// This converts the NomadEnvironment into the expected schema for dto.ExecutionEnvironmentData.
|
||||||
|
func (n *NomadEnvironment) MarshalJSON() (res []byte, err error) {
|
||||||
|
networkAccess, exposedPorts := n.NetworkAccess()
|
||||||
|
|
||||||
|
res, err = json.Marshal(dto.ExecutionEnvironmentData{
|
||||||
|
ID: int(n.ID()),
|
||||||
|
ExecutionEnvironmentRequest: dto.ExecutionEnvironmentRequest{
|
||||||
|
PrewarmingPoolSize: n.PrewarmingPoolSize(),
|
||||||
|
CPULimit: n.CPULimit(),
|
||||||
|
MemoryLimit: n.MemoryLimit(),
|
||||||
|
Image: n.Image(),
|
||||||
|
NetworkAccess: networkAccess,
|
||||||
|
ExposedPorts: exposedPorts,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return res, fmt.Errorf("couldn't marshal execution environment: %w", err)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyJob clones the native Nomad job in a way that it can be used as Runner job.
|
||||||
|
func (n *NomadEnvironment) DeepCopyJob() *nomadApi.Job {
|
||||||
|
copyJob, err := parseJob(n.jobHCL)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("The HCL of an existing environment should throw no error!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copyEnvironment := &NomadEnvironment{job: copyJob}
|
||||||
|
|
||||||
|
copyEnvironment.SetConfigFrom(n)
|
||||||
|
return copyEnvironment.job
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) SetConfigFrom(environment runner.ExecutionEnvironment) {
|
||||||
|
n.SetID(environment.ID())
|
||||||
|
n.SetPrewarmingPoolSize(environment.PrewarmingPoolSize())
|
||||||
|
n.SetCPULimit(environment.CPULimit())
|
||||||
|
n.SetMemoryLimit(environment.MemoryLimit())
|
||||||
|
n.SetImage(environment.Image())
|
||||||
|
n.SetNetworkAccess(environment.NetworkAccess())
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJob(jobHCL string) (*nomadApi.Job, error) {
|
||||||
|
config := jobspec2.ParseConfig{
|
||||||
|
Body: []byte(jobHCL),
|
||||||
|
AllowFS: false,
|
||||||
|
Strict: true,
|
||||||
|
}
|
||||||
|
job, err := jobspec2.ParseWithConfig(&config)
|
||||||
|
if err != nil {
|
||||||
|
return job, fmt.Errorf("couldn't parse job HCL: %w", err)
|
||||||
|
}
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) createRunners(apiClient nomad.ExecutorAPI, count uint) error {
|
||||||
|
log.WithField("runnersRequired", count).WithField("id", n.ID()).Debug("Creating new runners")
|
||||||
|
for i := 0; i < int(count); i++ {
|
||||||
|
err := n.createRunner(apiClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create new runner: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) createRunner(apiClient nomad.ExecutorAPI) error {
|
||||||
|
newUUID, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed generating runner id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newRunnerID := nomad.RunnerJobID(n.ID(), newUUID.String())
|
||||||
|
template := n.DeepCopyJob()
|
||||||
|
template.ID = &newRunnerID
|
||||||
|
template.Name = &newRunnerID
|
||||||
|
|
||||||
|
err = apiClient.RegisterRunnerJob(template)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error registering new runner job: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NomadEnvironment) removeRunners(apiClient nomad.ExecutorAPI, count uint) error {
|
||||||
|
log.WithField("runnersToDelete", count).WithField("id", n.ID()).Debug("Removing idle runners")
|
||||||
|
for i := 0; i < int(count); i++ {
|
||||||
|
r, ok := n.idleRunners.Sample()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not delete expected idle runner: %w", runner.ErrRunnerNotFound)
|
||||||
|
}
|
||||||
|
err := apiClient.DeleteJob(r.ID())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not delete expected Nomad idle runner: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
192
internal/environment/environment_test.go
Normal file
192
internal/environment/environment_test.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
package environment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
|
"github.com/openHPI/poseidon/internal/runner"
|
||||||
|
"github.com/openHPI/poseidon/tests"
|
||||||
|
"github.com/openHPI/poseidon/tests/helpers"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigureNetworkCreatesNewNetworkWhenNoNetworkExists(t *testing.T) {
|
||||||
|
_, job := helpers.CreateTemplateJob()
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(job)
|
||||||
|
environment := &NomadEnvironment{"", job, nil}
|
||||||
|
|
||||||
|
if assert.Equal(t, 0, len(defaultTaskGroup.Networks)) {
|
||||||
|
environment.SetNetworkAccess(true, []uint16{})
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(defaultTaskGroup.Networks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigureNetworkDoesNotCreateNewNetworkWhenNetworkExists(t *testing.T) {
|
||||||
|
_, job := helpers.CreateTemplateJob()
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(job)
|
||||||
|
environment := &NomadEnvironment{"", job, nil}
|
||||||
|
|
||||||
|
networkResource := &nomadApi.NetworkResource{Mode: "bridge"}
|
||||||
|
defaultTaskGroup.Networks = []*nomadApi.NetworkResource{networkResource}
|
||||||
|
|
||||||
|
if assert.Equal(t, 1, len(defaultTaskGroup.Networks)) {
|
||||||
|
environment.SetNetworkAccess(true, []uint16{})
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(defaultTaskGroup.Networks))
|
||||||
|
assert.Equal(t, networkResource, defaultTaskGroup.Networks[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigureNetworkSetsCorrectValues(t *testing.T) {
|
||||||
|
_, job := helpers.CreateTemplateJob()
|
||||||
|
defaultTaskGroup := nomad.FindOrCreateDefaultTaskGroup(job)
|
||||||
|
defaultTask := nomad.FindOrCreateDefaultTask(defaultTaskGroup)
|
||||||
|
|
||||||
|
mode, ok := defaultTask.Config["network_mode"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "none", mode)
|
||||||
|
assert.Equal(t, 0, len(defaultTaskGroup.Networks))
|
||||||
|
|
||||||
|
exposedPortsTests := [][]uint16{{}, {1337}, {42, 1337}}
|
||||||
|
t.Run("with no network access", func(t *testing.T) {
|
||||||
|
for _, ports := range exposedPortsTests {
|
||||||
|
_, testJob := helpers.CreateTemplateJob()
|
||||||
|
testTaskGroup := nomad.FindOrCreateDefaultTaskGroup(testJob)
|
||||||
|
testTask := nomad.FindOrCreateDefaultTask(testTaskGroup)
|
||||||
|
testEnvironment := &NomadEnvironment{"", job, nil}
|
||||||
|
|
||||||
|
testEnvironment.SetNetworkAccess(false, ports)
|
||||||
|
mode, ok := testTask.Config["network_mode"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "none", mode)
|
||||||
|
assert.Equal(t, 0, len(testTaskGroup.Networks))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with network access", func(t *testing.T) {
|
||||||
|
for _, ports := range exposedPortsTests {
|
||||||
|
_, testJob := helpers.CreateTemplateJob()
|
||||||
|
testTaskGroup := nomad.FindOrCreateDefaultTaskGroup(testJob)
|
||||||
|
testTask := nomad.FindOrCreateDefaultTask(testTaskGroup)
|
||||||
|
testEnvironment := &NomadEnvironment{"", testJob, nil}
|
||||||
|
|
||||||
|
testEnvironment.SetNetworkAccess(true, ports)
|
||||||
|
require.Equal(t, 1, len(testTaskGroup.Networks))
|
||||||
|
|
||||||
|
networkResource := testTaskGroup.Networks[0]
|
||||||
|
assert.Equal(t, "bridge", networkResource.Mode)
|
||||||
|
require.Equal(t, len(ports), len(networkResource.DynamicPorts))
|
||||||
|
|
||||||
|
assertExpectedPorts(t, ports, networkResource)
|
||||||
|
|
||||||
|
mode, ok := testTask.Config["network_mode"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, mode, "")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertExpectedPorts(t *testing.T, expectedPorts []uint16, networkResource *nomadApi.NetworkResource) {
|
||||||
|
t.Helper()
|
||||||
|
for _, expectedPort := range expectedPorts {
|
||||||
|
found := false
|
||||||
|
for _, actualPort := range networkResource.DynamicPorts {
|
||||||
|
if actualPort.To == int(expectedPort) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, fmt.Sprintf("port list should contain %v", expectedPort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterFailsWhenNomadJobRegistrationFails(t *testing.T) {
|
||||||
|
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||||
|
expectedErr := tests.ErrDefault
|
||||||
|
|
||||||
|
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", expectedErr)
|
||||||
|
|
||||||
|
environment := &NomadEnvironment{"", &nomadApi.Job{}, nil}
|
||||||
|
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
err := environment.Register(apiClientMock)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, expectedErr)
|
||||||
|
apiClientMock.AssertNotCalled(t, "EvaluationStream")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterTemplateJobSucceedsWhenMonitoringEvaluationSucceeds(t *testing.T) {
|
||||||
|
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||||
|
evaluationID := "id"
|
||||||
|
|
||||||
|
stream := make(chan *nomadApi.Events)
|
||||||
|
readonlyStream := func() <-chan *nomadApi.Events {
|
||||||
|
return stream
|
||||||
|
}()
|
||||||
|
// Immediately close stream to avoid any reading from it resulting in endless wait
|
||||||
|
close(stream)
|
||||||
|
|
||||||
|
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
||||||
|
apiClientMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.Anything).Return(nil)
|
||||||
|
apiClientMock.On("EvaluationStream", evaluationID, mock.AnythingOfType("*context.emptyCtx")).
|
||||||
|
Return(readonlyStream, nil)
|
||||||
|
|
||||||
|
environment := &NomadEnvironment{"", &nomadApi.Job{}, nil}
|
||||||
|
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
err := environment.Register(apiClientMock)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterTemplateJobReturnsErrorWhenMonitoringEvaluationFails(t *testing.T) {
|
||||||
|
apiClientMock := &nomad.ExecutorAPIMock{}
|
||||||
|
evaluationID := "id"
|
||||||
|
|
||||||
|
apiClientMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
||||||
|
apiClientMock.On("MonitorEvaluation", mock.AnythingOfType("string"), mock.Anything).Return(tests.ErrDefault)
|
||||||
|
|
||||||
|
environment := &NomadEnvironment{"", &nomadApi.Job{}, nil}
|
||||||
|
environment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
err := environment.Register(apiClientMock)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, err, tests.ErrDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseJob(t *testing.T) {
|
||||||
|
t.Run("parses the given default job", func(t *testing.T) {
|
||||||
|
environment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, environment.job)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error when given wrong job", func(t *testing.T) {
|
||||||
|
environment, err := NewNomadEnvironment("")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, environment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTwoSampleAddExactlyTwoRunners(t *testing.T) {
|
||||||
|
apiMock := &nomad.ExecutorAPIMock{}
|
||||||
|
apiMock.On("RegisterRunnerJob", mock.AnythingOfType("*api.Job")).Return(nil)
|
||||||
|
|
||||||
|
_, job := helpers.CreateTemplateJob()
|
||||||
|
environment := &NomadEnvironment{templateEnvironmentJobHCL, job, runner.NewLocalRunnerStorage()}
|
||||||
|
runner1 := &runner.RunnerMock{}
|
||||||
|
runner1.On("ID").Return(tests.DefaultRunnerID)
|
||||||
|
runner2 := &runner.RunnerMock{}
|
||||||
|
runner2.On("ID").Return(tests.AnotherRunnerID)
|
||||||
|
|
||||||
|
environment.AddRunner(runner1)
|
||||||
|
environment.AddRunner(runner2)
|
||||||
|
|
||||||
|
_, ok := environment.Sample(apiMock)
|
||||||
|
require.True(t, ok)
|
||||||
|
_, ok = environment.Sample(apiMock)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
apiMock.AssertNumberOfCalls(t, "RegisterRunnerJob", 2)
|
||||||
|
}
|
@ -3,15 +3,12 @@ package environment
|
|||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
|
||||||
"github.com/hashicorp/nomad/jobspec2"
|
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
"github.com/openHPI/poseidon/internal/nomad"
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
"github.com/openHPI/poseidon/internal/runner"
|
"github.com/openHPI/poseidon/internal/runner"
|
||||||
"github.com/openHPI/poseidon/pkg/dto"
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"github.com/openHPI/poseidon/pkg/logging"
|
"github.com/openHPI/poseidon/pkg/logging"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// templateEnvironmentJobHCL holds our default job in HCL format.
|
// templateEnvironmentJobHCL holds our default job in HCL format.
|
||||||
@ -28,13 +25,31 @@ type Manager interface {
|
|||||||
// It should be called during the startup process (e.g. on creation of the Manager).
|
// It should be called during the startup process (e.g. on creation of the Manager).
|
||||||
Load() error
|
Load() error
|
||||||
|
|
||||||
|
// List returns all environments known by Poseidon.
|
||||||
|
// When `fetch` is set the environments are fetched from the executor before returning.
|
||||||
|
List(fetch bool) ([]runner.ExecutionEnvironment, error)
|
||||||
|
|
||||||
|
// Get returns the details of the requested environment.
|
||||||
|
// When `fetch` is set the requested environment is fetched from the executor before returning.
|
||||||
|
Get(id dto.EnvironmentID, fetch bool) (runner.ExecutionEnvironment, error)
|
||||||
|
|
||||||
// CreateOrUpdate creates/updates an execution environment on the executor.
|
// CreateOrUpdate creates/updates an execution environment on the executor.
|
||||||
// If the job was created, the returned boolean is true, if it was updated, it is false.
|
// If the job was created, the returned boolean is true, if it was updated, it is false.
|
||||||
// If err is not nil, that means the environment was neither created nor updated.
|
// If err is not nil, that means the environment was neither created nor updated.
|
||||||
CreateOrUpdate(
|
CreateOrUpdate(
|
||||||
id runner.EnvironmentID,
|
id dto.EnvironmentID,
|
||||||
request dto.ExecutionEnvironmentRequest,
|
request dto.ExecutionEnvironmentRequest,
|
||||||
) (bool, error)
|
) (bool, error)
|
||||||
|
|
||||||
|
// Delete removes the specified execution environment.
|
||||||
|
// Iff the specified environment could not be found Delete returns false.
|
||||||
|
Delete(id dto.EnvironmentID) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NomadEnvironmentManager struct {
|
||||||
|
runnerManager runner.Manager
|
||||||
|
api nomad.ExecutorAPI
|
||||||
|
templateEnvironmentHCL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNomadEnvironmentManager(
|
func NewNomadEnvironmentManager(
|
||||||
@ -45,11 +60,8 @@ func NewNomadEnvironmentManager(
|
|||||||
if err := loadTemplateEnvironmentJobHCL(templateJobFile); err != nil {
|
if err := loadTemplateEnvironmentJobHCL(templateJobFile); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
templateEnvironmentJob, err := parseJob(templateEnvironmentJobHCL)
|
|
||||||
if err != nil {
|
m := &NomadEnvironmentManager{runnerManager, apiClient, templateEnvironmentJobHCL}
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m := &NomadEnvironmentManager{runnerManager, apiClient, *templateEnvironmentJob}
|
|
||||||
if err := m.Load(); err != nil {
|
if err := m.Load(); err != nil {
|
||||||
log.WithError(err).Error("Error recovering the execution environments")
|
log.WithError(err).Error("Error recovering the execution environments")
|
||||||
}
|
}
|
||||||
@ -57,6 +69,121 @@ func NewNomadEnvironmentManager(
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *NomadEnvironmentManager) Get(id dto.EnvironmentID, fetch bool) (
|
||||||
|
executionEnvironment runner.ExecutionEnvironment, err error) {
|
||||||
|
executionEnvironment, ok := m.runnerManager.GetEnvironment(id)
|
||||||
|
|
||||||
|
if fetch {
|
||||||
|
fetchedEnvironment, err := fetchEnvironment(id, m.api)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
case fetchedEnvironment == nil:
|
||||||
|
_, err = m.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ok = false
|
||||||
|
case !ok:
|
||||||
|
m.runnerManager.SetEnvironment(fetchedEnvironment)
|
||||||
|
executionEnvironment = fetchedEnvironment
|
||||||
|
ok = true
|
||||||
|
default:
|
||||||
|
executionEnvironment.SetConfigFrom(fetchedEnvironment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
err = runner.ErrUnknownExecutionEnvironment
|
||||||
|
}
|
||||||
|
return executionEnvironment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NomadEnvironmentManager) List(fetch bool) ([]runner.ExecutionEnvironment, error) {
|
||||||
|
if fetch {
|
||||||
|
err := m.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.runnerManager.ListEnvironments(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NomadEnvironmentManager) CreateOrUpdate(id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest) (
|
||||||
|
created bool, err error) {
|
||||||
|
environment, ok := m.runnerManager.GetEnvironment(id)
|
||||||
|
if !ok {
|
||||||
|
environment, err = NewNomadEnvironment(m.templateEnvironmentHCL)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error creating Nomad environment: %w", err)
|
||||||
|
}
|
||||||
|
environment.SetID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
environment.SetPrewarmingPoolSize(request.PrewarmingPoolSize)
|
||||||
|
environment.SetCPULimit(request.CPULimit)
|
||||||
|
environment.SetMemoryLimit(request.MemoryLimit)
|
||||||
|
environment.SetImage(request.Image)
|
||||||
|
environment.SetNetworkAccess(request.NetworkAccess, request.ExposedPorts)
|
||||||
|
created = m.runnerManager.SetEnvironment(environment)
|
||||||
|
|
||||||
|
err = environment.Register(m.api)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error registering template job in API: %w", err)
|
||||||
|
}
|
||||||
|
err = environment.UpdateRunnerSpecs(m.api)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error updating runner jobs in API: %w", err)
|
||||||
|
}
|
||||||
|
err = environment.Scale(m.api)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error scaling template job in API: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NomadEnvironmentManager) Delete(id dto.EnvironmentID) (bool, error) {
|
||||||
|
executionEnvironment, ok := m.runnerManager.GetEnvironment(id)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
m.runnerManager.DeleteEnvironment(id)
|
||||||
|
err := executionEnvironment.Delete(m.api)
|
||||||
|
if err != nil {
|
||||||
|
return true, fmt.Errorf("could not delete environment: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *NomadEnvironmentManager) Load() error {
|
||||||
|
templateJobs, err := m.api.LoadEnvironmentJobs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't load template jobs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range templateJobs {
|
||||||
|
jobLogger := log.WithField("jobID", *job.ID)
|
||||||
|
if *job.Status != structs.JobStatusRunning {
|
||||||
|
jobLogger.Info("Job not running, skipping ...")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configTaskGroup := nomad.FindOrCreateConfigTaskGroup(job)
|
||||||
|
if configTaskGroup == nil {
|
||||||
|
jobLogger.Info("Couldn't find config task group in job, skipping ...")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
environment := &NomadEnvironment{
|
||||||
|
jobHCL: templateEnvironmentJobHCL,
|
||||||
|
job: job,
|
||||||
|
idleRunners: runner.NewLocalRunnerStorage(),
|
||||||
|
}
|
||||||
|
m.runnerManager.SetEnvironment(environment)
|
||||||
|
jobLogger.Info("Successfully recovered environment")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// loadTemplateEnvironmentJobHCL loads the template environment job HCL from the given path.
|
// loadTemplateEnvironmentJobHCL loads the template environment job HCL from the given path.
|
||||||
// If the path is empty, the embedded default file is used.
|
// If the path is empty, the embedded default file is used.
|
||||||
func loadTemplateEnvironmentJobHCL(path string) error {
|
func loadTemplateEnvironmentJobHCL(path string) error {
|
||||||
@ -71,84 +198,25 @@ func loadTemplateEnvironmentJobHCL(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type NomadEnvironmentManager struct {
|
func fetchEnvironment(id dto.EnvironmentID, apiClient nomad.ExecutorAPI) (runner.ExecutionEnvironment, error) {
|
||||||
runnerManager runner.Manager
|
environments, err := apiClient.LoadEnvironmentJobs()
|
||||||
api nomad.ExecutorAPI
|
|
||||||
templateEnvironmentJob nomadApi.Job
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadEnvironmentManager) CreateOrUpdate(
|
|
||||||
id runner.EnvironmentID,
|
|
||||||
request dto.ExecutionEnvironmentRequest,
|
|
||||||
) (bool, error) {
|
|
||||||
templateJob, err := m.api.RegisterTemplateJob(&m.templateEnvironmentJob, runner.TemplateJobID(id),
|
|
||||||
request.PrewarmingPoolSize, request.CPULimit, request.MemoryLimit,
|
|
||||||
request.Image, request.NetworkAccess, request.ExposedPorts)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error registering template job in API: %w", err)
|
return nil, fmt.Errorf("error fetching the environment jobs: %w", err)
|
||||||
}
|
}
|
||||||
|
var fetchedEnvironment runner.ExecutionEnvironment
|
||||||
created, err := m.runnerManager.CreateOrUpdateEnvironment(id, request.PrewarmingPoolSize, templateJob, true)
|
for _, job := range environments {
|
||||||
if err != nil {
|
environmentID, err := nomad.EnvironmentIDFromTemplateJobID(*job.ID)
|
||||||
return created, fmt.Errorf("error updating environment in runner manager: %w", err)
|
if err != nil {
|
||||||
|
log.WithError(err).Warn("Cannot parse environment id of loaded environment")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if id == environmentID {
|
||||||
|
fetchedEnvironment = &NomadEnvironment{
|
||||||
|
jobHCL: templateEnvironmentJobHCL,
|
||||||
|
job: job,
|
||||||
|
idleRunners: runner.NewLocalRunnerStorage(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return created, nil
|
return fetchedEnvironment, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadEnvironmentManager) Load() error {
|
|
||||||
templateJobs, err := m.api.LoadEnvironmentJobs()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("couldn't load template jobs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, job := range templateJobs {
|
|
||||||
jobLogger := log.WithField("jobID", *job.ID)
|
|
||||||
if *job.Status != structs.JobStatusRunning {
|
|
||||||
jobLogger.Info("Job not running, skipping ...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
configTaskGroup := nomad.FindConfigTaskGroup(job)
|
|
||||||
if configTaskGroup == nil {
|
|
||||||
jobLogger.Info("Couldn't find config task group in job, skipping ...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
desiredIdleRunnersCount, err := strconv.Atoi(configTaskGroup.Meta[nomad.ConfigMetaPoolSizeKey])
|
|
||||||
if err != nil {
|
|
||||||
jobLogger.Infof("Couldn't convert pool size to int: %v, skipping ...", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
environmentIDString, err := runner.EnvironmentIDFromTemplateJobID(*job.ID)
|
|
||||||
if err != nil {
|
|
||||||
jobLogger.WithError(err).Error("Couldn't retrieve environment id from template job")
|
|
||||||
}
|
|
||||||
environmentID, err := runner.NewEnvironmentID(environmentIDString)
|
|
||||||
if err != nil {
|
|
||||||
jobLogger.WithField("environmentID", environmentIDString).
|
|
||||||
WithError(err).
|
|
||||||
Error("Couldn't retrieve environmentID from string")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, err = m.runnerManager.CreateOrUpdateEnvironment(environmentID, uint(desiredIdleRunnersCount), job, false)
|
|
||||||
if err != nil {
|
|
||||||
jobLogger.WithError(err).Info("Could not recover job.")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
jobLogger.Info("Successfully recovered environment")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseJob(jobHCL string) (*nomadApi.Job, error) {
|
|
||||||
config := jobspec2.ParseConfig{
|
|
||||||
Body: []byte(jobHCL),
|
|
||||||
AllowFS: false,
|
|
||||||
Strict: true,
|
|
||||||
}
|
|
||||||
job, err := jobspec2.ParseWithConfig(&config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing Nomad job: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return job, nil
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v2.8.0. DO NOT EDIT.
|
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||||
|
|
||||||
package environment
|
package environment
|
||||||
|
|
||||||
@ -15,18 +15,18 @@ type ManagerMock struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateOrUpdate provides a mock function with given fields: id, request
|
// CreateOrUpdate provides a mock function with given fields: id, request
|
||||||
func (_m *ManagerMock) CreateOrUpdate(id runner.EnvironmentID, request dto.ExecutionEnvironmentRequest) (bool, error) {
|
func (_m *ManagerMock) CreateOrUpdate(id dto.EnvironmentID, request dto.ExecutionEnvironmentRequest) (bool, error) {
|
||||||
ret := _m.Called(id, request)
|
ret := _m.Called(id, request)
|
||||||
|
|
||||||
var r0 bool
|
var r0 bool
|
||||||
if rf, ok := ret.Get(0).(func(runner.EnvironmentID, dto.ExecutionEnvironmentRequest) bool); ok {
|
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, dto.ExecutionEnvironmentRequest) bool); ok {
|
||||||
r0 = rf(id, request)
|
r0 = rf(id, request)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Get(0).(bool)
|
r0 = ret.Get(0).(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(runner.EnvironmentID, dto.ExecutionEnvironmentRequest) error); ok {
|
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, dto.ExecutionEnvironmentRequest) error); ok {
|
||||||
r1 = rf(id, request)
|
r1 = rf(id, request)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
@ -36,8 +36,70 @@ func (_m *ManagerMock) CreateOrUpdate(id runner.EnvironmentID, request dto.Execu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete provides a mock function with given fields: id
|
// Delete provides a mock function with given fields: id
|
||||||
func (_m *ManagerMock) Delete(id string) {
|
func (_m *ManagerMock) Delete(id dto.EnvironmentID) (bool, error) {
|
||||||
_m.Called(id)
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func(dto.EnvironmentID) bool); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(dto.EnvironmentID) error); ok {
|
||||||
|
r1 = rf(id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provides a mock function with given fields: id, fetch
|
||||||
|
func (_m *ManagerMock) Get(id dto.EnvironmentID, fetch bool) (runner.ExecutionEnvironment, error) {
|
||||||
|
ret := _m.Called(id, fetch)
|
||||||
|
|
||||||
|
var r0 runner.ExecutionEnvironment
|
||||||
|
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, bool) runner.ExecutionEnvironment); ok {
|
||||||
|
r0 = rf(id, fetch)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(runner.ExecutionEnvironment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, bool) error); ok {
|
||||||
|
r1 = rf(id, fetch)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// List provides a mock function with given fields: fetch
|
||||||
|
func (_m *ManagerMock) List(fetch bool) ([]runner.ExecutionEnvironment, error) {
|
||||||
|
ret := _m.Called(fetch)
|
||||||
|
|
||||||
|
var r0 []runner.ExecutionEnvironment
|
||||||
|
if rf, ok := ret.Get(0).(func(bool) []runner.ExecutionEnvironment); ok {
|
||||||
|
r0 = rf(fetch)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]runner.ExecutionEnvironment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(bool) error); ok {
|
||||||
|
r1 = rf(fetch)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load provides a mock function with given fields:
|
// Load provides a mock function with given fields:
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package environment
|
package environment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
"github.com/openHPI/poseidon/internal/nomad"
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
"github.com/openHPI/poseidon/internal/runner"
|
"github.com/openHPI/poseidon/internal/runner"
|
||||||
"github.com/openHPI/poseidon/pkg/dto"
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
@ -12,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateOrUpdateTestSuite struct {
|
type CreateOrUpdateTestSuite struct {
|
||||||
@ -20,7 +23,7 @@ type CreateOrUpdateTestSuite struct {
|
|||||||
apiMock nomad.ExecutorAPIMock
|
apiMock nomad.ExecutorAPIMock
|
||||||
request dto.ExecutionEnvironmentRequest
|
request dto.ExecutionEnvironmentRequest
|
||||||
manager *NomadEnvironmentManager
|
manager *NomadEnvironmentManager
|
||||||
environmentID runner.EnvironmentID
|
environmentID dto.EnvironmentID
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateOrUpdateTestSuite(t *testing.T) {
|
func TestCreateOrUpdateTestSuite(t *testing.T) {
|
||||||
@ -41,97 +44,22 @@ func (s *CreateOrUpdateTestSuite) SetupTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.manager = &NomadEnvironmentManager{
|
s.manager = &NomadEnvironmentManager{
|
||||||
runnerManager: &s.runnerManagerMock,
|
runnerManager: &s.runnerManagerMock,
|
||||||
api: &s.apiMock,
|
api: &s.apiMock,
|
||||||
|
templateEnvironmentHCL: templateEnvironmentJobHCL,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.environmentID = runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
s.environmentID = dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) mockRegisterTemplateJob(job *nomadApi.Job, err error) {
|
|
||||||
s.apiMock.On("RegisterTemplateJob",
|
|
||||||
mock.AnythingOfType("*api.Job"), mock.AnythingOfType("string"),
|
|
||||||
mock.AnythingOfType("uint"), mock.AnythingOfType("uint"), mock.AnythingOfType("uint"),
|
|
||||||
mock.AnythingOfType("string"), mock.AnythingOfType("bool"), mock.AnythingOfType("[]uint16")).
|
|
||||||
Return(job, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) mockCreateOrUpdateEnvironment(created bool, err error) {
|
|
||||||
s.runnerManagerMock.On("CreateOrUpdateEnvironment", mock.AnythingOfType("EnvironmentID"),
|
|
||||||
mock.AnythingOfType("uint"), mock.AnythingOfType("*api.Job"), mock.AnythingOfType("bool")).
|
|
||||||
Return(created, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) TestRegistersCorrectTemplateJob() {
|
|
||||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
|
||||||
s.mockCreateOrUpdateEnvironment(true, nil)
|
|
||||||
|
|
||||||
_, err := s.manager.CreateOrUpdate(s.environmentID, s.request)
|
|
||||||
s.NoError(err)
|
|
||||||
|
|
||||||
s.apiMock.AssertCalled(s.T(), "RegisterTemplateJob",
|
|
||||||
&s.manager.templateEnvironmentJob, runner.TemplateJobID(s.environmentID),
|
|
||||||
s.request.PrewarmingPoolSize, s.request.CPULimit, s.request.MemoryLimit,
|
|
||||||
s.request.Image, s.request.NetworkAccess, s.request.ExposedPorts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) TestReturnsErrorWhenRegisterTemplateJobReturnsError() {
|
|
||||||
s.mockRegisterTemplateJob(nil, tests.ErrDefault)
|
|
||||||
|
|
||||||
created, err := s.manager.CreateOrUpdate(s.environmentID, s.request)
|
|
||||||
s.ErrorIs(err, tests.ErrDefault)
|
|
||||||
s.False(created)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) TestCreatesOrUpdatesCorrectEnvironment() {
|
|
||||||
templateJobID := tests.DefaultJobID
|
|
||||||
templateJob := &nomadApi.Job{ID: &templateJobID}
|
|
||||||
s.mockRegisterTemplateJob(templateJob, nil)
|
|
||||||
s.mockCreateOrUpdateEnvironment(true, nil)
|
|
||||||
|
|
||||||
_, err := s.manager.CreateOrUpdate(s.environmentID, s.request)
|
|
||||||
s.NoError(err)
|
|
||||||
s.runnerManagerMock.AssertCalled(s.T(), "CreateOrUpdateEnvironment",
|
|
||||||
s.environmentID, s.request.PrewarmingPoolSize, templateJob, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) TestReturnsErrorIfCreatesOrUpdateEnvironmentReturnsError() {
|
func (s *CreateOrUpdateTestSuite) TestReturnsErrorIfCreatesOrUpdateEnvironmentReturnsError() {
|
||||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
s.apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", tests.ErrDefault)
|
||||||
s.mockCreateOrUpdateEnvironment(false, tests.ErrDefault)
|
s.runnerManagerMock.On("GetEnvironment", mock.AnythingOfType("dto.EnvironmentID")).Return(nil, false)
|
||||||
_, err := s.manager.CreateOrUpdate(runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
s.runnerManagerMock.On("SetEnvironment", mock.AnythingOfType("*environment.NomadEnvironment")).Return(true)
|
||||||
|
_, err := s.manager.CreateOrUpdate(dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
||||||
s.ErrorIs(err, tests.ErrDefault)
|
s.ErrorIs(err, tests.ErrDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) TestReturnsTrueIfCreatesOrUpdateEnvironmentReturnsTrue() {
|
|
||||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
|
||||||
s.mockCreateOrUpdateEnvironment(true, nil)
|
|
||||||
created, err := s.manager.CreateOrUpdate(runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.True(created)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CreateOrUpdateTestSuite) TestReturnsFalseIfCreatesOrUpdateEnvironmentReturnsFalse() {
|
|
||||||
s.mockRegisterTemplateJob(&nomadApi.Job{}, nil)
|
|
||||||
s.mockCreateOrUpdateEnvironment(false, nil)
|
|
||||||
created, err := s.manager.CreateOrUpdate(runner.EnvironmentID(tests.DefaultEnvironmentIDAsInteger), s.request)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.False(created)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseJob(t *testing.T) {
|
|
||||||
t.Run("parses the given default job", func(t *testing.T) {
|
|
||||||
job, err := parseJob(templateEnvironmentJobHCL)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NotNil(t, job)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("returns error when given wrong job", func(t *testing.T) {
|
|
||||||
job, err := parseJob("")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Nil(t, job)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewNomadEnvironmentManager(t *testing.T) {
|
func TestNewNomadEnvironmentManager(t *testing.T) {
|
||||||
executorAPIMock := &nomad.ExecutorAPIMock{}
|
executorAPIMock := &nomad.ExecutorAPIMock{}
|
||||||
executorAPIMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
executorAPIMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
||||||
@ -148,7 +76,7 @@ func TestNewNomadEnvironmentManager(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("loads template environment job from file", func(t *testing.T) {
|
t.Run("loads template environment job from file", func(t *testing.T) {
|
||||||
templateJobHCL := "job \"test\" {}"
|
templateJobHCL := "job \"test\" {}"
|
||||||
expectedJob, err := parseJob(templateJobHCL)
|
_, err := NewNomadEnvironment(templateJobHCL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
f := createTempFile(t, templateJobHCL)
|
f := createTempFile(t, templateJobHCL)
|
||||||
defer os.Remove(f.Name())
|
defer os.Remove(f.Name())
|
||||||
@ -156,8 +84,7 @@ func TestNewNomadEnvironmentManager(t *testing.T) {
|
|||||||
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, m)
|
assert.NotNil(t, m)
|
||||||
assert.Equal(t, templateJobHCL, templateEnvironmentJobHCL)
|
assert.Equal(t, templateJobHCL, m.templateEnvironmentHCL)
|
||||||
assert.Equal(t, *expectedJob, m.templateEnvironmentJob)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("returns error if template file is invalid", func(t *testing.T) {
|
t.Run("returns error if template file is invalid", func(t *testing.T) {
|
||||||
@ -165,13 +92,153 @@ func TestNewNomadEnvironmentManager(t *testing.T) {
|
|||||||
f := createTempFile(t, templateJobHCL)
|
f := createTempFile(t, templateJobHCL)
|
||||||
defer os.Remove(f.Name())
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
_, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
m, err := NewNomadEnvironmentManager(runnerManagerMock, executorAPIMock, f.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = NewNomadEnvironment(m.templateEnvironmentHCL)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
templateEnvironmentJobHCL = previousTemplateEnvironmentJobHCL
|
templateEnvironmentJobHCL = previousTemplateEnvironmentJobHCL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNomadEnvironmentManager_Get(t *testing.T) {
|
||||||
|
apiMock := &nomad.ExecutorAPIMock{}
|
||||||
|
mockWatchAllocations(apiMock)
|
||||||
|
call := apiMock.On("LoadEnvironmentJobs")
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
|
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{}, nil}
|
||||||
|
})
|
||||||
|
|
||||||
|
runnerManager := runner.NewNomadRunnerManager(apiMock, context.Background())
|
||||||
|
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("Returns error when not found", func(t *testing.T) {
|
||||||
|
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Returns environment when it was added before", func(t *testing.T) {
|
||||||
|
expectedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
expectedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
runnerManager.SetEnvironment(expectedEnvironment)
|
||||||
|
|
||||||
|
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedEnvironment, environment)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Fetch", func(t *testing.T) {
|
||||||
|
apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||||
|
t.Run("Returns error when not found", func(t *testing.T) {
|
||||||
|
_, err := m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Updates values when environment already known by Poseidon", func(t *testing.T) {
|
||||||
|
fetchedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fetchedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
fetchedEnvironment.SetImage("random docker image")
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
|
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{fetchedEnvironment.job}, nil}
|
||||||
|
})
|
||||||
|
|
||||||
|
localEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
runnerManager.SetEnvironment(localEnvironment)
|
||||||
|
|
||||||
|
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, fetchedEnvironment.Image(), environment.Image())
|
||||||
|
|
||||||
|
environment, err = m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, fetchedEnvironment.Image(), environment.Image())
|
||||||
|
})
|
||||||
|
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
|
||||||
|
t.Run("Adds environment when not already known by Poseidon", func(t *testing.T) {
|
||||||
|
fetchedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fetchedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
fetchedEnvironment.SetImage("random docker image")
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
|
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{fetchedEnvironment.job}, nil}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err = m.Get(tests.DefaultEnvironmentIDAsInteger, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
environment, err := m.Get(tests.DefaultEnvironmentIDAsInteger, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, fetchedEnvironment.Image(), environment.Image())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNomadEnvironmentManager_List(t *testing.T) {
|
||||||
|
apiMock := &nomad.ExecutorAPIMock{}
|
||||||
|
mockWatchAllocations(apiMock)
|
||||||
|
call := apiMock.On("LoadEnvironmentJobs")
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
|
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{}, nil}
|
||||||
|
})
|
||||||
|
|
||||||
|
runnerManager := runner.NewNomadRunnerManager(apiMock, context.Background())
|
||||||
|
m, err := NewNomadEnvironmentManager(runnerManager, apiMock, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("with no environments", func(t *testing.T) {
|
||||||
|
environments, err := m.List(true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, environments)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Returns added environment", func(t *testing.T) {
|
||||||
|
localEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
localEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
runnerManager.SetEnvironment(localEnvironment)
|
||||||
|
|
||||||
|
environments, err := m.List(false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(environments))
|
||||||
|
assert.Equal(t, localEnvironment, environments[0])
|
||||||
|
})
|
||||||
|
runnerManager.DeleteEnvironment(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
|
||||||
|
t.Run("Fetches new Runners via the api client", func(t *testing.T) {
|
||||||
|
fetchedEnvironment, err := NewNomadEnvironment(templateEnvironmentJobHCL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fetchedEnvironment.SetID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
status := structs.JobStatusRunning
|
||||||
|
fetchedEnvironment.job.Status = &status
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
|
call.ReturnArguments = mock.Arguments{[]*nomadApi.Job{fetchedEnvironment.job}, nil}
|
||||||
|
})
|
||||||
|
|
||||||
|
environments, err := m.List(false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, environments)
|
||||||
|
|
||||||
|
environments, err = m.List(true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(environments))
|
||||||
|
assert.Equal(t, fetchedEnvironment, environments[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockWatchAllocations(apiMock *nomad.ExecutorAPIMock) {
|
||||||
|
call := apiMock.On("WatchAllocations", mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
call.Run(func(args mock.Arguments) {
|
||||||
|
<-time.After(10 * time.Minute) // 10 minutes is the default test timeout
|
||||||
|
call.ReturnArguments = mock.Arguments{nil}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func createTempFile(t *testing.T, content string) *os.File {
|
func createTempFile(t *testing.T, content string) *os.File {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
f, err := os.CreateTemp("", "test")
|
f, err := os.CreateTemp("", "test")
|
||||||
|
@ -27,8 +27,8 @@ type apiQuerier interface {
|
|||||||
// SetJobScale sets the scaling count of the passed job to Nomad.
|
// SetJobScale sets the scaling count of the passed job to Nomad.
|
||||||
SetJobScale(jobID string, count uint, reason string) (err error)
|
SetJobScale(jobID string, count uint, reason string) (err error)
|
||||||
|
|
||||||
// DeleteRunner deletes the runner with the given ID.
|
// DeleteJob deletes the Job with the given ID.
|
||||||
DeleteRunner(runnerID string) (err error)
|
DeleteJob(jobID string) (err error)
|
||||||
|
|
||||||
// Execute runs a command in the passed job.
|
// Execute runs a command in the passed job.
|
||||||
Execute(jobID string, ctx context.Context, command []string, tty bool,
|
Execute(jobID string, ctx context.Context, command []string, tty bool,
|
||||||
@ -82,8 +82,8 @@ func (nc *nomadAPIClient) init(nomadConfig *config.Nomad) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nc *nomadAPIClient) DeleteRunner(runnerID string) (err error) {
|
func (nc *nomadAPIClient) DeleteJob(jobID string) (err error) {
|
||||||
_, _, err = nc.client.Jobs().Deregister(runnerID, true, nc.writeOptions())
|
_, _, err = nc.client.Jobs().Deregister(jobID, true, nc.writeOptions())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func (_m *apiQuerierMock) AllocationStream(ctx context.Context) (<-chan *api.Eve
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRunner provides a mock function with given fields: runnerID
|
// DeleteRunner provides a mock function with given fields: runnerID
|
||||||
func (_m *apiQuerierMock) DeleteRunner(runnerID string) error {
|
func (_m *apiQuerierMock) DeleteJob(runnerID string) error {
|
||||||
ret := _m.Called(runnerID)
|
ret := _m.Called(runnerID)
|
||||||
|
|
||||||
var r0 error
|
var r0 error
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
|
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||||
|
|
||||||
package nomad
|
package nomad
|
||||||
|
|
||||||
@ -41,13 +41,13 @@ func (_m *ExecutorAPIMock) AllocationStream(ctx context.Context) (<-chan *api.Ev
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRunner provides a mock function with given fields: runnerID
|
// DeleteJob provides a mock function with given fields: jobID
|
||||||
func (_m *ExecutorAPIMock) DeleteRunner(runnerID string) error {
|
func (_m *ExecutorAPIMock) DeleteJob(jobID string) error {
|
||||||
ret := _m.Called(runnerID)
|
ret := _m.Called(jobID)
|
||||||
|
|
||||||
var r0 error
|
var r0 error
|
||||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
r0 = rf(runnerID)
|
r0 = rf(jobID)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Error(0)
|
||||||
}
|
}
|
||||||
@ -319,29 +319,6 @@ func (_m *ExecutorAPIMock) RegisterRunnerJob(template *api.Job) error {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterTemplateJob provides a mock function with given fields: defaultJob, id, prewarmingPoolSize, cpuLimit, memoryLimit, image, networkAccess, exposedPorts
|
|
||||||
func (_m *ExecutorAPIMock) RegisterTemplateJob(defaultJob *api.Job, id string, prewarmingPoolSize uint, cpuLimit uint, memoryLimit uint, image string, networkAccess bool, exposedPorts []uint16) (*api.Job, error) {
|
|
||||||
ret := _m.Called(defaultJob, id, prewarmingPoolSize, cpuLimit, memoryLimit, image, networkAccess, exposedPorts)
|
|
||||||
|
|
||||||
var r0 *api.Job
|
|
||||||
if rf, ok := ret.Get(0).(func(*api.Job, string, uint, uint, uint, string, bool, []uint16) *api.Job); ok {
|
|
||||||
r0 = rf(defaultJob, id, prewarmingPoolSize, cpuLimit, memoryLimit, image, networkAccess, exposedPorts)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).(*api.Job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(*api.Job, string, uint, uint, uint, string, bool, []uint16) error); ok {
|
|
||||||
r1 = rf(defaultJob, id, prewarmingPoolSize, cpuLimit, memoryLimit, image, networkAccess, exposedPorts)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetJobScale provides a mock function with given fields: jobID, count, reason
|
// SetJobScale provides a mock function with given fields: jobID, count, reason
|
||||||
func (_m *ExecutorAPIMock) SetJobScale(jobID string, count uint, reason string) error {
|
func (_m *ExecutorAPIMock) SetJobScale(jobID string, count uint, reason string) error {
|
||||||
ret := _m.Called(jobID, count, reason)
|
ret := _m.Called(jobID, count, reason)
|
||||||
|
@ -5,7 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -24,73 +26,19 @@ const (
|
|||||||
ConfigMetaUnusedValue = "false"
|
ConfigMetaUnusedValue = "false"
|
||||||
ConfigMetaTimeoutKey = "timeout"
|
ConfigMetaTimeoutKey = "timeout"
|
||||||
ConfigMetaPoolSizeKey = "prewarmingPoolSize"
|
ConfigMetaPoolSizeKey = "prewarmingPoolSize"
|
||||||
|
TemplateJobNameParts = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
TaskArgs = []string{"infinity"}
|
ErrorInvalidJobID = errors.New("invalid job id")
|
||||||
ErrorConfigTaskGroupNotFound = errors.New("config task group not found in job")
|
TaskArgs = []string{"infinity"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// FindConfigTaskGroup returns the config task group of a job.
|
|
||||||
// The config task group should be included in all jobs.
|
|
||||||
func FindConfigTaskGroup(job *nomadApi.Job) *nomadApi.TaskGroup {
|
|
||||||
for _, tg := range job.TaskGroups {
|
|
||||||
if *tg.Name == ConfigTaskGroupName {
|
|
||||||
return tg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetMetaConfigValue(job *nomadApi.Job, key, value string) error {
|
|
||||||
configTaskGroup := FindConfigTaskGroup(job)
|
|
||||||
if configTaskGroup == nil {
|
|
||||||
return ErrorConfigTaskGroupNotFound
|
|
||||||
}
|
|
||||||
configTaskGroup.Meta[key] = value
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterTemplateJob creates a Nomad job based on the default job configuration and the given parameters.
|
|
||||||
// It registers the job with Nomad and waits until the registration completes.
|
|
||||||
func (a *APIClient) RegisterTemplateJob(
|
|
||||||
basisJob *nomadApi.Job,
|
|
||||||
id string,
|
|
||||||
prewarmingPoolSize, cpuLimit, memoryLimit uint,
|
|
||||||
image string,
|
|
||||||
networkAccess bool,
|
|
||||||
exposedPorts []uint16) (*nomadApi.Job, error) {
|
|
||||||
job := CreateTemplateJob(basisJob, id, prewarmingPoolSize,
|
|
||||||
cpuLimit, memoryLimit, image, networkAccess, exposedPorts)
|
|
||||||
evalID, err := a.apiQuerier.RegisterNomadJob(job)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("couldn't register template job: %w", err)
|
|
||||||
}
|
|
||||||
return job, a.MonitorEvaluation(evalID, context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTemplateJob creates a Nomad job based on the default job configuration and the given parameters.
|
|
||||||
// It registers the job with Nomad and waits until the registration completes.
|
|
||||||
func CreateTemplateJob(
|
|
||||||
basisJob *nomadApi.Job,
|
|
||||||
id string,
|
|
||||||
prewarmingPoolSize, cpuLimit, memoryLimit uint,
|
|
||||||
image string,
|
|
||||||
networkAccess bool,
|
|
||||||
exposedPorts []uint16) *nomadApi.Job {
|
|
||||||
job := *basisJob
|
|
||||||
job.ID = &id
|
|
||||||
job.Name = &id
|
|
||||||
|
|
||||||
var taskGroup = createTaskGroup(&job, TaskGroupName)
|
|
||||||
configureTask(taskGroup, TaskName, cpuLimit, memoryLimit, image, networkAccess, exposedPorts)
|
|
||||||
storeTemplateConfiguration(&job, prewarmingPoolSize)
|
|
||||||
|
|
||||||
return &job
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *APIClient) RegisterRunnerJob(template *nomadApi.Job) error {
|
func (a *APIClient) RegisterRunnerJob(template *nomadApi.Job) error {
|
||||||
storeRunnerConfiguration(template)
|
taskGroup := FindOrCreateConfigTaskGroup(template)
|
||||||
|
|
||||||
|
taskGroup.Meta = make(map[string]string)
|
||||||
|
taskGroup.Meta[ConfigMetaUsedKey] = ConfigMetaUnusedValue
|
||||||
|
|
||||||
evalID, err := a.apiQuerier.RegisterNomadJob(template)
|
evalID, err := a.apiQuerier.RegisterNomadJob(template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -99,126 +47,37 @@ func (a *APIClient) RegisterRunnerJob(template *nomadApi.Job) error {
|
|||||||
return a.MonitorEvaluation(evalID, context.Background())
|
return a.MonitorEvaluation(evalID, context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTaskGroup(job *nomadApi.Job, name string) *nomadApi.TaskGroup {
|
func FindTaskGroup(job *nomadApi.Job, name string) *nomadApi.TaskGroup {
|
||||||
var taskGroup *nomadApi.TaskGroup
|
for _, tg := range job.TaskGroups {
|
||||||
if len(job.TaskGroups) == 0 {
|
if *tg.Name == name {
|
||||||
taskGroup = nomadApi.NewTaskGroup(name, TaskCount)
|
return tg
|
||||||
job.TaskGroups = []*nomadApi.TaskGroup{taskGroup}
|
}
|
||||||
} else {
|
|
||||||
taskGroup = job.TaskGroups[0]
|
|
||||||
taskGroup.Name = &name
|
|
||||||
count := TaskCount
|
|
||||||
taskGroup.Count = &count
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindOrCreateDefaultTaskGroup(job *nomadApi.Job) *nomadApi.TaskGroup {
|
||||||
|
taskGroup := FindTaskGroup(job, TaskGroupName)
|
||||||
|
if taskGroup == nil {
|
||||||
|
taskGroup = nomadApi.NewTaskGroup(TaskGroupName, TaskCount)
|
||||||
|
job.AddTaskGroup(taskGroup)
|
||||||
|
}
|
||||||
|
FindOrCreateDefaultTask(taskGroup)
|
||||||
return taskGroup
|
return taskGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
const portNumberBase = 10
|
func FindOrCreateConfigTaskGroup(job *nomadApi.Job) *nomadApi.TaskGroup {
|
||||||
|
taskGroup := FindTaskGroup(job, ConfigTaskGroupName)
|
||||||
func configureNetwork(taskGroup *nomadApi.TaskGroup, networkAccess bool, exposedPorts []uint16) {
|
|
||||||
if len(taskGroup.Tasks) == 0 {
|
|
||||||
// This function is only used internally and must be called as last step when configuring the task.
|
|
||||||
// This error is not recoverable.
|
|
||||||
log.Fatal("Can't configure network before task has been configured!")
|
|
||||||
}
|
|
||||||
task := taskGroup.Tasks[0]
|
|
||||||
|
|
||||||
if task.Config == nil {
|
|
||||||
task.Config = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if networkAccess {
|
|
||||||
var networkResource *nomadApi.NetworkResource
|
|
||||||
if len(taskGroup.Networks) == 0 {
|
|
||||||
networkResource = &nomadApi.NetworkResource{}
|
|
||||||
taskGroup.Networks = []*nomadApi.NetworkResource{networkResource}
|
|
||||||
} else {
|
|
||||||
networkResource = taskGroup.Networks[0]
|
|
||||||
}
|
|
||||||
// Prefer "bridge" network over "host" to have an isolated network namespace with bridged interface
|
|
||||||
// instead of joining the host network namespace.
|
|
||||||
networkResource.Mode = "bridge"
|
|
||||||
for _, portNumber := range exposedPorts {
|
|
||||||
port := nomadApi.Port{
|
|
||||||
Label: strconv.FormatUint(uint64(portNumber), portNumberBase),
|
|
||||||
To: int(portNumber),
|
|
||||||
}
|
|
||||||
networkResource.DynamicPorts = append(networkResource.DynamicPorts, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly set mode to override existing settings when updating job from without to with network.
|
|
||||||
// Don't use bridge as it collides with the bridge mode above. This results in Docker using 'bridge'
|
|
||||||
// mode, meaning all allocations will be attached to the `docker0` adapter and could reach other
|
|
||||||
// non-Nomad containers attached to it. This is avoided when using Nomads bridge network mode.
|
|
||||||
task.Config["network_mode"] = ""
|
|
||||||
} else {
|
|
||||||
// Somehow, we can't set the network mode to none in the NetworkResource on task group level.
|
|
||||||
// See https://github.com/hashicorp/nomad/issues/10540
|
|
||||||
task.Config["network_mode"] = "none"
|
|
||||||
// Explicitly set Networks to signal Nomad to remove the possibly existing networkResource
|
|
||||||
taskGroup.Networks = []*nomadApi.NetworkResource{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func configureTask(
|
|
||||||
taskGroup *nomadApi.TaskGroup,
|
|
||||||
name string,
|
|
||||||
cpuLimit, memoryLimit uint,
|
|
||||||
image string,
|
|
||||||
networkAccess bool,
|
|
||||||
exposedPorts []uint16) {
|
|
||||||
var task *nomadApi.Task
|
|
||||||
if len(taskGroup.Tasks) == 0 {
|
|
||||||
task = nomadApi.NewTask(name, TaskDriver)
|
|
||||||
taskGroup.Tasks = []*nomadApi.Task{task}
|
|
||||||
} else {
|
|
||||||
task = taskGroup.Tasks[0]
|
|
||||||
task.Name = name
|
|
||||||
}
|
|
||||||
integerCPULimit := int(cpuLimit)
|
|
||||||
integerMemoryLimit := int(memoryLimit)
|
|
||||||
if task.Resources == nil {
|
|
||||||
task.Resources = nomadApi.DefaultResources()
|
|
||||||
}
|
|
||||||
task.Resources.CPU = &integerCPULimit
|
|
||||||
task.Resources.MemoryMB = &integerMemoryLimit
|
|
||||||
|
|
||||||
if task.Config == nil {
|
|
||||||
task.Config = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
task.Config["image"] = image
|
|
||||||
task.Config["command"] = TaskCommand
|
|
||||||
task.Config["args"] = TaskArgs
|
|
||||||
|
|
||||||
configureNetwork(taskGroup, networkAccess, exposedPorts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func storeTemplateConfiguration(job *nomadApi.Job, prewarmingPoolSize uint) {
|
|
||||||
taskGroup := findOrCreateConfigTaskGroup(job)
|
|
||||||
|
|
||||||
taskGroup.Meta = make(map[string]string)
|
|
||||||
taskGroup.Meta[ConfigMetaPoolSizeKey] = strconv.Itoa(int(prewarmingPoolSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
func storeRunnerConfiguration(job *nomadApi.Job) {
|
|
||||||
taskGroup := findOrCreateConfigTaskGroup(job)
|
|
||||||
|
|
||||||
taskGroup.Meta = make(map[string]string)
|
|
||||||
taskGroup.Meta[ConfigMetaUsedKey] = ConfigMetaUnusedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func findOrCreateConfigTaskGroup(job *nomadApi.Job) *nomadApi.TaskGroup {
|
|
||||||
taskGroup := FindConfigTaskGroup(job)
|
|
||||||
if taskGroup == nil {
|
if taskGroup == nil {
|
||||||
taskGroup = nomadApi.NewTaskGroup(ConfigTaskGroupName, 0)
|
taskGroup = nomadApi.NewTaskGroup(ConfigTaskGroupName, 0)
|
||||||
job.AddTaskGroup(taskGroup)
|
job.AddTaskGroup(taskGroup)
|
||||||
}
|
}
|
||||||
createConfigTaskIfNotPresent(taskGroup)
|
FindOrCreateConfigTask(taskGroup)
|
||||||
return taskGroup
|
return taskGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// createConfigTaskIfNotPresent ensures that a dummy task is in the task group so that the group is accepted by Nomad.
|
// FindOrCreateConfigTask ensures that a dummy task is in the task group so that the group is accepted by Nomad.
|
||||||
func createConfigTaskIfNotPresent(taskGroup *nomadApi.TaskGroup) {
|
func FindOrCreateConfigTask(taskGroup *nomadApi.TaskGroup) *nomadApi.Task {
|
||||||
var task *nomadApi.Task
|
var task *nomadApi.Task
|
||||||
for _, t := range taskGroup.Tasks {
|
for _, t := range taskGroup.Tasks {
|
||||||
if t.Name == ConfigTaskName {
|
if t.Name == ConfigTaskName {
|
||||||
@ -236,4 +95,75 @@ func createConfigTaskIfNotPresent(taskGroup *nomadApi.TaskGroup) {
|
|||||||
task.Config = make(map[string]interface{})
|
task.Config = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
task.Config["command"] = ConfigTaskCommand
|
task.Config["command"] = ConfigTaskCommand
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOrCreateDefaultTask ensures that a default task is in the task group in that the executions are made.
|
||||||
|
func FindOrCreateDefaultTask(taskGroup *nomadApi.TaskGroup) *nomadApi.Task {
|
||||||
|
var task *nomadApi.Task
|
||||||
|
for _, t := range taskGroup.Tasks {
|
||||||
|
if t.Name == TaskName {
|
||||||
|
task = t
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if task == nil {
|
||||||
|
task = nomadApi.NewTask(TaskName, TaskDriver)
|
||||||
|
taskGroup.Tasks = append(taskGroup.Tasks, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Resources == nil {
|
||||||
|
task.Resources = nomadApi.DefaultResources()
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Config == nil {
|
||||||
|
task.Config = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
task.Config["command"] = TaskCommand
|
||||||
|
task.Config["args"] = TaskArgs
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnvironmentTemplateID checks if the passed job id belongs to a template job.
|
||||||
|
func IsEnvironmentTemplateID(jobID string) bool {
|
||||||
|
parts := strings.Split(jobID, "-")
|
||||||
|
if len(parts) != TemplateJobNameParts || parts[0] != TemplateJobPrefix {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := EnvironmentIDFromTemplateJobID(jobID)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunnerJobID returns the nomad job id of the runner with the given environmentID and id.
|
||||||
|
func RunnerJobID(environmentID dto.EnvironmentID, id string) string {
|
||||||
|
return fmt.Sprintf("%d-%s", environmentID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateJobID returns the id of the nomad job for the environment with the given id.
|
||||||
|
func TemplateJobID(id dto.EnvironmentID) string {
|
||||||
|
return fmt.Sprintf("%s-%d", TemplateJobPrefix, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentIDFromRunnerID returns the environment id that is part of the passed runner job id.
|
||||||
|
func EnvironmentIDFromRunnerID(jobID string) (dto.EnvironmentID, error) {
|
||||||
|
return partOfJobID(jobID, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentIDFromTemplateJobID returns the environment id that is part of the passed environment job id.
|
||||||
|
func EnvironmentIDFromTemplateJobID(id string) (dto.EnvironmentID, error) {
|
||||||
|
return partOfJobID(id, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func partOfJobID(id string, part uint) (dto.EnvironmentID, error) {
|
||||||
|
parts := strings.Split(id, "-")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return 0, fmt.Errorf("empty job id: %w", ErrorInvalidJobID)
|
||||||
|
}
|
||||||
|
environmentID, err := strconv.Atoi(parts[part])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid environment id par %v: %w", err, ErrorInvalidJobID)
|
||||||
|
}
|
||||||
|
return dto.EnvironmentID(environmentID), nil
|
||||||
}
|
}
|
||||||
|
@ -1,279 +1,121 @@
|
|||||||
package nomad
|
package nomad
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
"github.com/openHPI/poseidon/tests"
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"github.com/openHPI/poseidon/tests/helpers"
|
"github.com/openHPI/poseidon/tests/helpers"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createTestTaskGroup() *nomadApi.TaskGroup {
|
func TestFindTaskGroup(t *testing.T) {
|
||||||
return nomadApi.NewTaskGroup("taskGroup", 1)
|
t.Run("Returns nil if task group not found", func(t *testing.T) {
|
||||||
}
|
group := FindTaskGroup(&nomadApi.Job{}, TaskGroupName)
|
||||||
|
assert.Nil(t, group)
|
||||||
func createTestTask() *nomadApi.Task {
|
|
||||||
return nomadApi.NewTask("task", "docker")
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTestResources() *nomadApi.Resources {
|
|
||||||
result := nomadApi.DefaultResources()
|
|
||||||
expectedCPULimit := 1337
|
|
||||||
expectedMemoryLimit := 42
|
|
||||||
result.CPU = &expectedCPULimit
|
|
||||||
result.MemoryMB = &expectedMemoryLimit
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateTaskGroupCreatesNewTaskGroupWhenJobHasNoTaskGroup(t *testing.T) {
|
|
||||||
job := nomadApi.NewBatchJob("test", "test", "test", 1)
|
|
||||||
|
|
||||||
if assert.Equal(t, 0, len(job.TaskGroups)) {
|
|
||||||
expectedTaskGroup := createTestTaskGroup()
|
|
||||||
taskGroup := createTaskGroup(job, *expectedTaskGroup.Name)
|
|
||||||
|
|
||||||
assert.Equal(t, *expectedTaskGroup, *taskGroup)
|
|
||||||
assert.Equal(t, []*nomadApi.TaskGroup{taskGroup}, job.TaskGroups, "it should add the task group to the job")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateTaskGroupOverwritesOptionsWhenJobHasTaskGroup(t *testing.T) {
|
|
||||||
job := nomadApi.NewBatchJob("test", "test", "test", 1)
|
|
||||||
existingTaskGroup := createTestTaskGroup()
|
|
||||||
existingTaskGroup.Meta = map[string]string{"field": "should still exist"}
|
|
||||||
newTaskGroupList := []*nomadApi.TaskGroup{existingTaskGroup}
|
|
||||||
job.TaskGroups = newTaskGroupList
|
|
||||||
|
|
||||||
newName := *existingTaskGroup.Name + "longerName"
|
|
||||||
taskGroup := createTaskGroup(job, newName)
|
|
||||||
|
|
||||||
// create a new copy to avoid changing the original one as it is a pointer
|
|
||||||
expectedTaskGroup := *existingTaskGroup
|
|
||||||
expectedTaskGroup.Name = &newName
|
|
||||||
|
|
||||||
assert.Equal(t, expectedTaskGroup, *taskGroup)
|
|
||||||
assert.Equal(t, newTaskGroupList, job.TaskGroups, "it should not modify the jobs task group list")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigureNetworkFatalsWhenNoTaskExists(t *testing.T) {
|
|
||||||
logger, hook := test.NewNullLogger()
|
|
||||||
logger.ExitFunc = func(i int) {
|
|
||||||
panic(i)
|
|
||||||
}
|
|
||||||
log = logger.WithField("pkg", "job_test")
|
|
||||||
taskGroup := createTestTaskGroup()
|
|
||||||
if assert.Equal(t, 0, len(taskGroup.Tasks)) {
|
|
||||||
assert.Panics(t, func() {
|
|
||||||
configureNetwork(taskGroup, false, nil)
|
|
||||||
})
|
|
||||||
assert.Equal(t, logrus.FatalLevel, hook.LastEntry().Level)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigureNetworkCreatesNewNetworkWhenNoNetworkExists(t *testing.T) {
|
|
||||||
taskGroup := createTestTaskGroup()
|
|
||||||
task := createTestTask()
|
|
||||||
taskGroup.Tasks = []*nomadApi.Task{task}
|
|
||||||
|
|
||||||
if assert.Equal(t, 0, len(taskGroup.Networks)) {
|
|
||||||
configureNetwork(taskGroup, true, []uint16{})
|
|
||||||
|
|
||||||
assert.Equal(t, 1, len(taskGroup.Networks))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigureNetworkDoesNotCreateNewNetworkWhenNetworkExists(t *testing.T) {
|
|
||||||
taskGroup := createTestTaskGroup()
|
|
||||||
task := createTestTask()
|
|
||||||
taskGroup.Tasks = []*nomadApi.Task{task}
|
|
||||||
networkResource := &nomadApi.NetworkResource{Mode: "bridge"}
|
|
||||||
taskGroup.Networks = []*nomadApi.NetworkResource{networkResource}
|
|
||||||
|
|
||||||
if assert.Equal(t, 1, len(taskGroup.Networks)) {
|
|
||||||
configureNetwork(taskGroup, true, []uint16{})
|
|
||||||
|
|
||||||
assert.Equal(t, 1, len(taskGroup.Networks))
|
|
||||||
assert.Equal(t, networkResource, taskGroup.Networks[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigureNetworkSetsCorrectValues(t *testing.T) {
|
|
||||||
taskGroup := createTestTaskGroup()
|
|
||||||
task := createTestTask()
|
|
||||||
_, ok := task.Config["network_mode"]
|
|
||||||
|
|
||||||
require.False(t, ok, "Test tasks network_mode should not be set")
|
|
||||||
|
|
||||||
taskGroup.Tasks = []*nomadApi.Task{task}
|
|
||||||
exposedPortsTests := [][]uint16{{}, {1337}, {42, 1337}}
|
|
||||||
|
|
||||||
t.Run("with no network access", func(t *testing.T) {
|
|
||||||
for _, ports := range exposedPortsTests {
|
|
||||||
testTaskGroup := *taskGroup
|
|
||||||
testTask := *task
|
|
||||||
testTaskGroup.Tasks = []*nomadApi.Task{&testTask}
|
|
||||||
|
|
||||||
configureNetwork(&testTaskGroup, false, ports)
|
|
||||||
mode, ok := testTask.Config["network_mode"]
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, "none", mode)
|
|
||||||
assert.Equal(t, 0, len(testTaskGroup.Networks))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with network access", func(t *testing.T) {
|
t.Run("Finds task group when existent", func(t *testing.T) {
|
||||||
for _, ports := range exposedPortsTests {
|
_, job := helpers.CreateTemplateJob()
|
||||||
testTaskGroup := *taskGroup
|
group := FindTaskGroup(job, TaskGroupName)
|
||||||
testTask := *task
|
assert.NotNil(t, group)
|
||||||
testTaskGroup.Tasks = []*nomadApi.Task{&testTask}
|
|
||||||
|
|
||||||
configureNetwork(&testTaskGroup, true, ports)
|
|
||||||
require.Equal(t, 1, len(testTaskGroup.Networks))
|
|
||||||
|
|
||||||
networkResource := testTaskGroup.Networks[0]
|
|
||||||
assert.Equal(t, "bridge", networkResource.Mode)
|
|
||||||
require.Equal(t, len(ports), len(networkResource.DynamicPorts))
|
|
||||||
|
|
||||||
assertExpectedPorts(t, ports, networkResource)
|
|
||||||
|
|
||||||
mode, ok := testTask.Config["network_mode"]
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.Equal(t, mode, "")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertExpectedPorts(t *testing.T, expectedPorts []uint16, networkResource *nomadApi.NetworkResource) {
|
func TestFindOrCreateDefaultTask(t *testing.T) {
|
||||||
t.Helper()
|
t.Run("Adds default task group when not set", func(t *testing.T) {
|
||||||
for _, expectedPort := range expectedPorts {
|
job := &nomadApi.Job{}
|
||||||
found := false
|
group := FindOrCreateDefaultTaskGroup(job)
|
||||||
for _, actualPort := range networkResource.DynamicPorts {
|
assert.NotNil(t, group)
|
||||||
if actualPort.To == int(expectedPort) {
|
assert.Equal(t, TaskGroupName, *group.Name)
|
||||||
found = true
|
assert.Equal(t, 1, len(job.TaskGroups))
|
||||||
break
|
assert.Equal(t, group, job.TaskGroups[0])
|
||||||
}
|
assert.Equal(t, TaskCount, *group.Count)
|
||||||
}
|
})
|
||||||
assert.True(t, found, fmt.Sprintf("port list should contain %v", expectedPort))
|
|
||||||
}
|
t.Run("Does not modify task group when already set", func(t *testing.T) {
|
||||||
|
job := &nomadApi.Job{}
|
||||||
|
groupName := TaskGroupName
|
||||||
|
expectedGroup := &nomadApi.TaskGroup{Name: &groupName}
|
||||||
|
job.TaskGroups = []*nomadApi.TaskGroup{expectedGroup}
|
||||||
|
|
||||||
|
group := FindOrCreateDefaultTaskGroup(job)
|
||||||
|
assert.NotNil(t, group)
|
||||||
|
assert.Equal(t, 1, len(job.TaskGroups))
|
||||||
|
assert.Equal(t, expectedGroup, group)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigureTaskWhenNoTaskExists(t *testing.T) {
|
func TestFindOrCreateConfigTaskGroup(t *testing.T) {
|
||||||
taskGroup := createTestTaskGroup()
|
t.Run("Adds config task group when not set", func(t *testing.T) {
|
||||||
require.Equal(t, 0, len(taskGroup.Tasks))
|
job := &nomadApi.Job{}
|
||||||
|
group := FindOrCreateConfigTaskGroup(job)
|
||||||
|
assert.NotNil(t, group)
|
||||||
|
assert.Equal(t, group, job.TaskGroups[0])
|
||||||
|
assert.Equal(t, 1, len(job.TaskGroups))
|
||||||
|
|
||||||
expectedResources := createTestResources()
|
assert.Equal(t, ConfigTaskGroupName, *group.Name)
|
||||||
expectedTaskGroup := *taskGroup
|
assert.Equal(t, 0, *group.Count)
|
||||||
expectedTask := nomadApi.NewTask("task", TaskDriver)
|
})
|
||||||
expectedTask.Resources = expectedResources
|
|
||||||
expectedImage := "python:latest"
|
|
||||||
expectedCommand := "sleep"
|
|
||||||
expectedArgs := []string{"infinity"}
|
|
||||||
expectedTask.Config = map[string]interface{}{
|
|
||||||
"image": expectedImage, "command": expectedCommand, "args": expectedArgs, "network_mode": "none"}
|
|
||||||
expectedTaskGroup.Tasks = []*nomadApi.Task{expectedTask}
|
|
||||||
expectedTaskGroup.Networks = []*nomadApi.NetworkResource{}
|
|
||||||
|
|
||||||
configureTask(taskGroup, expectedTask.Name,
|
t.Run("Does not modify task group when already set", func(t *testing.T) {
|
||||||
uint(*expectedResources.CPU), uint(*expectedResources.MemoryMB),
|
job := &nomadApi.Job{}
|
||||||
expectedImage, false, []uint16{})
|
groupName := ConfigTaskGroupName
|
||||||
|
expectedGroup := &nomadApi.TaskGroup{Name: &groupName}
|
||||||
|
job.TaskGroups = []*nomadApi.TaskGroup{expectedGroup}
|
||||||
|
|
||||||
assert.Equal(t, expectedTaskGroup, *taskGroup)
|
group := FindOrCreateConfigTaskGroup(job)
|
||||||
|
assert.NotNil(t, group)
|
||||||
|
assert.Equal(t, 1, len(job.TaskGroups))
|
||||||
|
assert.Equal(t, expectedGroup, group)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigureTaskWhenTaskExists(t *testing.T) {
|
func TestFindOrCreateTask(t *testing.T) {
|
||||||
taskGroup := createTestTaskGroup()
|
t.Run("Does not modify default task when already set", func(t *testing.T) {
|
||||||
task := createTestTask()
|
groupName := TaskGroupName
|
||||||
task.Config = map[string]interface{}{"my_custom_config": "should not be overwritten"}
|
group := &nomadApi.TaskGroup{Name: &groupName}
|
||||||
taskGroup.Tasks = []*nomadApi.Task{task}
|
expectedTask := &nomadApi.Task{Name: TaskName}
|
||||||
require.Equal(t, 1, len(taskGroup.Tasks))
|
group.Tasks = []*nomadApi.Task{expectedTask}
|
||||||
|
|
||||||
expectedResources := createTestResources()
|
task := FindOrCreateDefaultTask(group)
|
||||||
expectedTaskGroup := *taskGroup
|
assert.NotNil(t, task)
|
||||||
expectedTask := *task
|
assert.Equal(t, 1, len(group.Tasks))
|
||||||
expectedTask.Resources = expectedResources
|
assert.Equal(t, expectedTask, task)
|
||||||
expectedImage := "python:latest"
|
})
|
||||||
expectedTask.Config["image"] = expectedImage
|
|
||||||
expectedTask.Config["network_mode"] = "none"
|
|
||||||
expectedTaskGroup.Tasks = []*nomadApi.Task{&expectedTask}
|
|
||||||
expectedTaskGroup.Networks = []*nomadApi.NetworkResource{}
|
|
||||||
|
|
||||||
configureTask(taskGroup, expectedTask.Name,
|
t.Run("Does not modify config task when already set", func(t *testing.T) {
|
||||||
uint(*expectedResources.CPU), uint(*expectedResources.MemoryMB),
|
groupName := ConfigTaskGroupName
|
||||||
expectedImage, false, []uint16{})
|
group := &nomadApi.TaskGroup{Name: &groupName}
|
||||||
|
expectedTask := &nomadApi.Task{Name: ConfigTaskName}
|
||||||
|
group.Tasks = []*nomadApi.Task{expectedTask}
|
||||||
|
|
||||||
assert.Equal(t, expectedTaskGroup, *taskGroup)
|
task := FindOrCreateConfigTask(group)
|
||||||
assert.Equal(t, task, taskGroup.Tasks[0], "it should not create a new task")
|
assert.NotNil(t, task)
|
||||||
|
assert.Equal(t, 1, len(group.Tasks))
|
||||||
|
assert.Equal(t, expectedTask, task)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateTemplateJobSetsAllGivenArguments(t *testing.T) {
|
func TestIsEnvironmentTemplateID(t *testing.T) {
|
||||||
base, testJob := helpers.CreateTemplateJob()
|
assert.True(t, IsEnvironmentTemplateID("template-42"))
|
||||||
prewarmingPoolSize, err := strconv.Atoi(testJob.TaskGroups[1].Meta[ConfigMetaPoolSizeKey])
|
assert.False(t, IsEnvironmentTemplateID("template-42-100"))
|
||||||
require.NoError(t, err)
|
assert.False(t, IsEnvironmentTemplateID("job-42"))
|
||||||
job := CreateTemplateJob(
|
assert.False(t, IsEnvironmentTemplateID("template-top"))
|
||||||
base,
|
|
||||||
tests.DefaultJobID,
|
|
||||||
uint(prewarmingPoolSize),
|
|
||||||
uint(*testJob.TaskGroups[0].Tasks[0].Resources.CPU),
|
|
||||||
uint(*testJob.TaskGroups[0].Tasks[0].Resources.MemoryMB),
|
|
||||||
testJob.TaskGroups[0].Tasks[0].Config["image"].(string),
|
|
||||||
false,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
assert.Equal(t, *testJob, *job)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterTemplateJobFailsWhenNomadJobRegistrationFails(t *testing.T) {
|
func TestRunnerJobID(t *testing.T) {
|
||||||
apiMock := apiQuerierMock{}
|
assert.Equal(t, "0-RANDOM-UUID", RunnerJobID(0, "RANDOM-UUID"))
|
||||||
expectedErr := tests.ErrDefault
|
|
||||||
|
|
||||||
apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return("", expectedErr)
|
|
||||||
|
|
||||||
apiClient := &APIClient{&apiMock}
|
|
||||||
|
|
||||||
_, err := apiClient.RegisterTemplateJob(&nomadApi.Job{}, tests.DefaultJobID,
|
|
||||||
1, 2, 3, "image", false, []uint16{})
|
|
||||||
assert.ErrorIs(t, err, expectedErr)
|
|
||||||
apiMock.AssertNotCalled(t, "EvaluationStream")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterTemplateJobSucceedsWhenMonitoringEvaluationSucceeds(t *testing.T) {
|
func TestTemplateJobID(t *testing.T) {
|
||||||
apiMock := apiQuerierMock{}
|
assert.Equal(t, "template-42", TemplateJobID(42))
|
||||||
evaluationID := "id"
|
}
|
||||||
|
|
||||||
stream := make(chan *nomadApi.Events)
|
func TestEnvironmentIDFromRunnerID(t *testing.T) {
|
||||||
readonlyStream := func() <-chan *nomadApi.Events {
|
id, err := EnvironmentIDFromRunnerID("42-RANDOM-UUID")
|
||||||
return stream
|
|
||||||
}()
|
|
||||||
// Immediately close stream to avoid any reading from it resulting in endless wait
|
|
||||||
close(stream)
|
|
||||||
|
|
||||||
apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
|
||||||
apiMock.On("EvaluationStream", evaluationID, mock.AnythingOfType("*context.emptyCtx")).
|
|
||||||
Return(readonlyStream, nil)
|
|
||||||
|
|
||||||
apiClient := &APIClient{&apiMock}
|
|
||||||
|
|
||||||
_, err := apiClient.RegisterTemplateJob(&nomadApi.Job{}, tests.DefaultJobID,
|
|
||||||
1, 2, 3, "image", false, []uint16{})
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
assert.Equal(t, dto.EnvironmentID(42), id)
|
||||||
|
|
||||||
func TestRegisterTemplateJobReturnsErrorWhenMonitoringEvaluationFails(t *testing.T) {
|
_, err = EnvironmentIDFromRunnerID("")
|
||||||
apiMock := apiQuerierMock{}
|
assert.Error(t, err)
|
||||||
evaluationID := "id"
|
|
||||||
|
|
||||||
apiMock.On("RegisterNomadJob", mock.AnythingOfType("*api.Job")).Return(evaluationID, nil)
|
|
||||||
apiMock.On("EvaluationStream", evaluationID, mock.AnythingOfType("*context.emptyCtx")).Return(nil, tests.ErrDefault)
|
|
||||||
|
|
||||||
apiClient := &APIClient{&apiMock}
|
|
||||||
|
|
||||||
_, err := apiClient.RegisterTemplateJob(&nomadApi.Job{}, tests.DefaultJobID,
|
|
||||||
1, 2, 3, "image", false, []uint16{})
|
|
||||||
assert.ErrorIs(t, err, tests.ErrDefault)
|
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ var (
|
|||||||
|
|
||||||
type AllocationProcessor func(*nomadApi.Allocation)
|
type AllocationProcessor func(*nomadApi.Allocation)
|
||||||
|
|
||||||
// ExecutorAPI provides access to an container orchestration solution.
|
// ExecutorAPI provides access to a container orchestration solution.
|
||||||
type ExecutorAPI interface {
|
type ExecutorAPI interface {
|
||||||
apiQuerier
|
apiQuerier
|
||||||
|
|
||||||
@ -42,12 +42,6 @@ type ExecutorAPI interface {
|
|||||||
// LoadRunnerPortMappings returns the mapped ports of the runner.
|
// LoadRunnerPortMappings returns the mapped ports of the runner.
|
||||||
LoadRunnerPortMappings(runnerID string) ([]nomadApi.PortMapping, error)
|
LoadRunnerPortMappings(runnerID string) ([]nomadApi.PortMapping, error)
|
||||||
|
|
||||||
// RegisterTemplateJob creates a template job based on the default job configuration and the given parameters.
|
|
||||||
// It registers the job and waits until the registration completes.
|
|
||||||
RegisterTemplateJob(defaultJob *nomadApi.Job, id string,
|
|
||||||
prewarmingPoolSize, cpuLimit, memoryLimit uint,
|
|
||||||
image string, networkAccess bool, exposedPorts []uint16) (*nomadApi.Job, error)
|
|
||||||
|
|
||||||
// RegisterRunnerJob creates a runner job based on the template job.
|
// RegisterRunnerJob creates a runner job based on the template job.
|
||||||
// It registers the job and waits until the registration completes.
|
// It registers the job and waits until the registration completes.
|
||||||
RegisterRunnerJob(template *nomadApi.Job) error
|
RegisterRunnerJob(template *nomadApi.Job) error
|
||||||
@ -278,14 +272,10 @@ func (a *APIClient) MarkRunnerAsUsed(runnerID string, duration int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't retrieve job info: %w", err)
|
return fmt.Errorf("couldn't retrieve job info: %w", err)
|
||||||
}
|
}
|
||||||
err = SetMetaConfigValue(job, ConfigMetaUsedKey, ConfigMetaUsedValue)
|
configTaskGroup := FindOrCreateConfigTaskGroup(job)
|
||||||
if err != nil {
|
configTaskGroup.Meta[ConfigMetaUsedKey] = ConfigMetaUsedValue
|
||||||
return fmt.Errorf("couldn't update runner in job as used: %w", err)
|
configTaskGroup.Meta[ConfigMetaTimeoutKey] = strconv.Itoa(duration)
|
||||||
}
|
|
||||||
err = SetMetaConfigValue(job, ConfigMetaTimeoutKey, strconv.Itoa(duration))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("couldn't update runner in job with timeout: %w", err)
|
|
||||||
}
|
|
||||||
_, err = a.RegisterNomadJob(job)
|
_, err = a.RegisterNomadJob(job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("couldn't update runner config: %w", err)
|
return fmt.Errorf("couldn't update runner config: %w", err)
|
||||||
|
@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -38,15 +37,15 @@ type LoadRunnersTestSuite struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *LoadRunnersTestSuite) SetupTest() {
|
func (s *LoadRunnersTestSuite) SetupTest() {
|
||||||
s.jobID = tests.DefaultJobID
|
s.jobID = tests.DefaultRunnerID
|
||||||
|
|
||||||
s.mock = &apiQuerierMock{}
|
s.mock = &apiQuerierMock{}
|
||||||
s.nomadAPIClient = APIClient{apiQuerier: s.mock}
|
s.nomadAPIClient = APIClient{apiQuerier: s.mock}
|
||||||
|
|
||||||
s.availableRunner = newJobListStub(tests.DefaultJobID, structs.JobStatusRunning, 1)
|
s.availableRunner = newJobListStub(tests.DefaultRunnerID, structs.JobStatusRunning, 1)
|
||||||
s.anotherAvailableRunner = newJobListStub(tests.AnotherJobID, structs.JobStatusRunning, 1)
|
s.anotherAvailableRunner = newJobListStub(tests.AnotherRunnerID, structs.JobStatusRunning, 1)
|
||||||
s.pendingRunner = newJobListStub(tests.DefaultJobID+"-1", structs.JobStatusPending, 0)
|
s.pendingRunner = newJobListStub(tests.DefaultRunnerID+"-1", structs.JobStatusPending, 0)
|
||||||
s.deadRunner = newJobListStub(tests.AnotherJobID+"-1", structs.JobStatusDead, 0)
|
s.deadRunner = newJobListStub(tests.AnotherRunnerID+"-1", structs.JobStatusDead, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newJobListStub(id, status string, amountRunning int) *nomadApi.JobListStub {
|
func newJobListStub(id, status string, amountRunning int) *nomadApi.JobListStub {
|
||||||
@ -122,13 +121,6 @@ func (s *LoadRunnersTestSuite) TestReturnsAllAvailableRunners() {
|
|||||||
s.Contains(returnedIds, s.anotherAvailableRunner.ID)
|
s.Contains(returnedIds, s.anotherAvailableRunner.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
TestURL = url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: "127.0.0.1:4646",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const TestNamespace = "unit-tests"
|
const TestNamespace = "unit-tests"
|
||||||
const TestNomadToken = "n0m4d-t0k3n"
|
const TestNomadToken = "n0m4d-t0k3n"
|
||||||
const TestDefaultAddress = "127.0.0.1"
|
const TestDefaultAddress = "127.0.0.1"
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import "github.com/openHPI/poseidon/tests"
|
import (
|
||||||
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
|
"github.com/openHPI/poseidon/tests"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultEnvironmentID = EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
defaultEnvironmentID = dto.EnvironmentID(tests.DefaultEnvironmentIDAsInteger)
|
||||||
anotherEnvironmentID = EnvironmentID(tests.AnotherEnvironmentIDAsInteger)
|
anotherEnvironmentID = dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger)
|
||||||
defaultInactivityTimeout = 0
|
defaultInactivityTimeout = 0
|
||||||
)
|
)
|
||||||
|
255
internal/runner/execution_environment_mock.go
Normal file
255
internal/runner/execution_environment_mock.go
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
dto "github.com/openHPI/poseidon/pkg/dto"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
nomad "github.com/openHPI/poseidon/internal/nomad"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecutionEnvironmentMock is an autogenerated mock type for the ExecutionEnvironment type
|
||||||
|
type ExecutionEnvironmentMock struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRunner provides a mock function with given fields: r
|
||||||
|
func (_m *ExecutionEnvironmentMock) AddRunner(r Runner) {
|
||||||
|
_m.Called(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPULimit provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) CPULimit() uint {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 uint
|
||||||
|
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(uint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete provides a mock function with given fields: apiClient
|
||||||
|
func (_m *ExecutionEnvironmentMock) Delete(apiClient nomad.ExecutorAPI) error {
|
||||||
|
ret := _m.Called(apiClient)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(nomad.ExecutorAPI) error); ok {
|
||||||
|
r0 = rf(apiClient)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRunner provides a mock function with given fields: id
|
||||||
|
func (_m *ExecutionEnvironmentMock) DeleteRunner(id string) {
|
||||||
|
_m.Called(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) ID() dto.EnvironmentID {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 dto.EnvironmentID
|
||||||
|
if rf, ok := ret.Get(0).(func() dto.EnvironmentID); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(dto.EnvironmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) Image() string {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
if rf, ok := ret.Get(0).(func() string); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) MarshalJSON() ([]byte, error) {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 []byte
|
||||||
|
if rf, ok := ret.Get(0).(func() []byte); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func() error); ok {
|
||||||
|
r1 = rf()
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryLimit provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) MemoryLimit() uint {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 uint
|
||||||
|
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(uint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkAccess provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) NetworkAccess() (bool, []uint16) {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 []uint16
|
||||||
|
if rf, ok := ret.Get(1).(func() []uint16); ok {
|
||||||
|
r1 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(1) != nil {
|
||||||
|
r1 = ret.Get(1).([]uint16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrewarmingPoolSize provides a mock function with given fields:
|
||||||
|
func (_m *ExecutionEnvironmentMock) PrewarmingPoolSize() uint {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 uint
|
||||||
|
if rf, ok := ret.Get(0).(func() uint); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(uint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register provides a mock function with given fields: apiClient
|
||||||
|
func (_m *ExecutionEnvironmentMock) Register(apiClient nomad.ExecutorAPI) error {
|
||||||
|
ret := _m.Called(apiClient)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(nomad.ExecutorAPI) error); ok {
|
||||||
|
r0 = rf(apiClient)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample provides a mock function with given fields: apiClient
|
||||||
|
func (_m *ExecutionEnvironmentMock) Sample(apiClient nomad.ExecutorAPI) (Runner, bool) {
|
||||||
|
ret := _m.Called(apiClient)
|
||||||
|
|
||||||
|
var r0 Runner
|
||||||
|
if rf, ok := ret.Get(0).(func(nomad.ExecutorAPI) Runner); ok {
|
||||||
|
r0 = rf(apiClient)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(Runner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 bool
|
||||||
|
if rf, ok := ret.Get(1).(func(nomad.ExecutorAPI) bool); ok {
|
||||||
|
r1 = rf(apiClient)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Get(1).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale provides a mock function with given fields: apiClient
|
||||||
|
func (_m *ExecutionEnvironmentMock) Scale(apiClient nomad.ExecutorAPI) error {
|
||||||
|
ret := _m.Called(apiClient)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(nomad.ExecutorAPI) error); ok {
|
||||||
|
r0 = rf(apiClient)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCPULimit provides a mock function with given fields: limit
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetCPULimit(limit uint) {
|
||||||
|
_m.Called(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfigFrom provides a mock function with given fields: environment
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetConfigFrom(environment ExecutionEnvironment) {
|
||||||
|
_m.Called(environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID provides a mock function with given fields: id
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetID(id dto.EnvironmentID) {
|
||||||
|
_m.Called(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetImage provides a mock function with given fields: image
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetImage(image string) {
|
||||||
|
_m.Called(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemoryLimit provides a mock function with given fields: limit
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetMemoryLimit(limit uint) {
|
||||||
|
_m.Called(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNetworkAccess provides a mock function with given fields: allow, ports
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetNetworkAccess(allow bool, ports []uint16) {
|
||||||
|
_m.Called(allow, ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrewarmingPoolSize provides a mock function with given fields: count
|
||||||
|
func (_m *ExecutionEnvironmentMock) SetPrewarmingPoolSize(count uint) {
|
||||||
|
_m.Called(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRunnerSpecs provides a mock function with given fields: apiClient
|
||||||
|
func (_m *ExecutionEnvironmentMock) UpdateRunnerSpecs(apiClient nomad.ExecutorAPI) error {
|
||||||
|
ret := _m.Called(apiClient)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(nomad.ExecutorAPI) error); ok {
|
||||||
|
r0 = rf(apiClient)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
@ -2,52 +2,89 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
"github.com/openHPI/poseidon/internal/nomad"
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"github.com/openHPI/poseidon/pkg/logging"
|
"github.com/openHPI/poseidon/pkg/logging"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log = logging.GetLogger("runner")
|
log = logging.GetLogger("runner")
|
||||||
ErrUnknownExecutionEnvironment = errors.New("execution environment not found")
|
ErrUnknownExecutionEnvironment = errors.New("execution environment not found")
|
||||||
ErrNoRunnersAvailable = errors.New("no runners available for this execution environment")
|
ErrNoRunnersAvailable = errors.New("no runners available for this execution environment")
|
||||||
ErrRunnerNotFound = errors.New("no runner found with this id")
|
ErrRunnerNotFound = errors.New("no runner found with this id")
|
||||||
ErrorUpdatingExecutionEnvironment = errors.New("errors occurred when updating environment")
|
|
||||||
ErrorInvalidJobID = errors.New("invalid job id")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvironmentID int
|
// ExecutionEnvironment are groups of runner that share the configuration stored in the environment.
|
||||||
|
type ExecutionEnvironment interface {
|
||||||
|
json.Marshaler
|
||||||
|
|
||||||
func NewEnvironmentID(id string) (EnvironmentID, error) {
|
// ID returns the id of the environment.
|
||||||
environment, err := strconv.Atoi(id)
|
ID() dto.EnvironmentID
|
||||||
return EnvironmentID(environment), err
|
SetID(id dto.EnvironmentID)
|
||||||
|
// PrewarmingPoolSize sets the number of idle runner of this environment that should be prewarmed.
|
||||||
|
PrewarmingPoolSize() uint
|
||||||
|
SetPrewarmingPoolSize(count uint)
|
||||||
|
// CPULimit sets the share of cpu that a runner should receive at minimum.
|
||||||
|
CPULimit() uint
|
||||||
|
SetCPULimit(limit uint)
|
||||||
|
// MemoryLimit sets the amount of memory that should be available for each runner.
|
||||||
|
MemoryLimit() uint
|
||||||
|
SetMemoryLimit(limit uint)
|
||||||
|
// Image sets the image of the runner, e.g. Docker image.
|
||||||
|
Image() string
|
||||||
|
SetImage(image string)
|
||||||
|
// NetworkAccess sets if a runner should have network access and if ports should be mapped.
|
||||||
|
NetworkAccess() (bool, []uint16)
|
||||||
|
SetNetworkAccess(allow bool, ports []uint16)
|
||||||
|
// SetConfigFrom copies all above attributes from the passed environment to the object itself.
|
||||||
|
SetConfigFrom(environment ExecutionEnvironment)
|
||||||
|
|
||||||
|
// Register saves this environment at the executor.
|
||||||
|
Register(apiClient nomad.ExecutorAPI) error
|
||||||
|
// Delete removes this environment and all it's runner from the executor and Poseidon itself.
|
||||||
|
Delete(apiClient nomad.ExecutorAPI) error
|
||||||
|
// Scale manages if the executor has enough idle runner according to the PrewarmingPoolSize.
|
||||||
|
Scale(apiClient nomad.ExecutorAPI) error
|
||||||
|
// UpdateRunnerSpecs updates all Runner of the passed environment to have the same definition as the environment.
|
||||||
|
UpdateRunnerSpecs(apiClient nomad.ExecutorAPI) error
|
||||||
|
|
||||||
|
// Sample returns and removes an arbitrary available runner.
|
||||||
|
// ok is true iff a runner was returned.
|
||||||
|
Sample(apiClient nomad.ExecutorAPI) (r Runner, ok bool)
|
||||||
|
// AddRunner adds an existing runner to the idle runners of the environment.
|
||||||
|
AddRunner(r Runner)
|
||||||
|
// DeleteRunner removes an idle runner from the environment.
|
||||||
|
DeleteRunner(id string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e EnvironmentID) toString() string {
|
|
||||||
return strconv.Itoa(int(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
type NomadJobID string
|
|
||||||
|
|
||||||
// Manager keeps track of the used and unused runners of all execution environments in order to provide unused
|
// Manager keeps track of the used and unused runners of all execution environments in order to provide unused
|
||||||
// runners to new clients and ensure no runner is used twice.
|
// runners to new clients and ensure no runner is used twice.
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
// CreateOrUpdateEnvironment creates the given environment if it does not exist. Otherwise, it updates
|
// ListEnvironments returns all execution environments known by Poseidon.
|
||||||
// the existing environment and all runners. Iff a new Environment has been created, it returns true.
|
ListEnvironments() []ExecutionEnvironment
|
||||||
// Iff scale is true, runners are created until the desiredIdleRunnersCount is reached.
|
|
||||||
CreateOrUpdateEnvironment(id EnvironmentID, desiredIdleRunnersCount uint, templateJob *nomadApi.Job,
|
// GetEnvironment returns the details of the requested environment.
|
||||||
scale bool) (bool, error)
|
// Iff the requested environment is not stored it returns false.
|
||||||
|
GetEnvironment(id dto.EnvironmentID) (ExecutionEnvironment, bool)
|
||||||
|
|
||||||
|
// SetEnvironment stores the environment in Poseidons memory.
|
||||||
|
// It returns true iff a new environment is stored and false iff an existing environment was updated.
|
||||||
|
SetEnvironment(environment ExecutionEnvironment) bool
|
||||||
|
|
||||||
|
// DeleteEnvironment removes the specified execution environment in Poseidons memory.
|
||||||
|
// It does nothing if the specified environment can not be found.
|
||||||
|
DeleteEnvironment(id dto.EnvironmentID)
|
||||||
|
|
||||||
// Claim returns a new runner. The runner is deleted after duration seconds if duration is not 0.
|
// Claim returns a new runner. The runner is deleted after duration seconds if duration is not 0.
|
||||||
// It makes sure that the runner is not in use yet and returns an error if no runner could be provided.
|
// It makes sure that the runner is not in use yet and returns an error if no runner could be provided.
|
||||||
Claim(id EnvironmentID, duration int) (Runner, error)
|
Claim(id dto.EnvironmentID, duration int) (Runner, error)
|
||||||
|
|
||||||
// Get returns the used runner with the given runnerId.
|
// Get returns the used runner with the given runnerId.
|
||||||
// If no runner with the given runnerId is currently used, it returns an error.
|
// If no runner with the given runnerId is currently used, it returns an error.
|
||||||
@ -64,7 +101,7 @@ type Manager interface {
|
|||||||
|
|
||||||
type NomadRunnerManager struct {
|
type NomadRunnerManager struct {
|
||||||
apiClient nomad.ExecutorAPI
|
apiClient nomad.ExecutorAPI
|
||||||
environments NomadEnvironmentStorage
|
environments EnvironmentStorage
|
||||||
usedRunners Storage
|
usedRunners Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,107 +111,37 @@ type NomadRunnerManager struct {
|
|||||||
func NewNomadRunnerManager(apiClient nomad.ExecutorAPI, ctx context.Context) *NomadRunnerManager {
|
func NewNomadRunnerManager(apiClient nomad.ExecutorAPI, ctx context.Context) *NomadRunnerManager {
|
||||||
m := &NomadRunnerManager{
|
m := &NomadRunnerManager{
|
||||||
apiClient,
|
apiClient,
|
||||||
NewLocalNomadEnvironmentStorage(),
|
NewLocalEnvironmentStorage(),
|
||||||
NewLocalRunnerStorage(),
|
NewLocalRunnerStorage(),
|
||||||
}
|
}
|
||||||
go m.keepRunnersSynced(ctx)
|
go m.keepRunnersSynced(ctx)
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
type NomadEnvironment struct {
|
func (m *NomadRunnerManager) ListEnvironments() []ExecutionEnvironment {
|
||||||
environmentID EnvironmentID
|
return m.environments.List()
|
||||||
idleRunners Storage
|
|
||||||
desiredIdleRunnersCount uint
|
|
||||||
templateJob *nomadApi.Job
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *NomadEnvironment) ID() EnvironmentID {
|
func (m *NomadRunnerManager) GetEnvironment(id dto.EnvironmentID) (ExecutionEnvironment, bool) {
|
||||||
return j.environmentID
|
return m.environments.Get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *NomadRunnerManager) CreateOrUpdateEnvironment(id EnvironmentID, desiredIdleRunnersCount uint,
|
func (m *NomadRunnerManager) SetEnvironment(environment ExecutionEnvironment) bool {
|
||||||
templateJob *nomadApi.Job, scale bool) (bool, error) {
|
_, ok := m.environments.Get(environment.ID())
|
||||||
_, ok := m.environments.Get(id)
|
m.environments.Add(environment)
|
||||||
if !ok {
|
return !ok
|
||||||
return true, m.registerEnvironment(id, desiredIdleRunnersCount, templateJob, scale)
|
|
||||||
}
|
|
||||||
return false, m.updateEnvironment(id, desiredIdleRunnersCount, templateJob, scale)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *NomadRunnerManager) registerEnvironment(environmentID EnvironmentID, desiredIdleRunnersCount uint,
|
func (m *NomadRunnerManager) DeleteEnvironment(id dto.EnvironmentID) {
|
||||||
templateJob *nomadApi.Job, scale bool) error {
|
m.environments.Delete(id)
|
||||||
m.environments.Add(&NomadEnvironment{
|
|
||||||
environmentID,
|
|
||||||
NewLocalRunnerStorage(),
|
|
||||||
desiredIdleRunnersCount,
|
|
||||||
templateJob,
|
|
||||||
})
|
|
||||||
if scale {
|
|
||||||
err := m.scaleEnvironment(environmentID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("couldn't upscale environment %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateEnvironment updates all runners of the specified environment. This is required as attributes like the
|
func (m *NomadRunnerManager) Claim(environmentID dto.EnvironmentID, duration int) (Runner, error) {
|
||||||
// CPULimit or MemoryMB could be changed in the new template job.
|
|
||||||
func (m *NomadRunnerManager) updateEnvironment(id EnvironmentID, desiredIdleRunnersCount uint,
|
|
||||||
newTemplateJob *nomadApi.Job, scale bool) error {
|
|
||||||
environment, ok := m.environments.Get(id)
|
|
||||||
if !ok {
|
|
||||||
return ErrUnknownExecutionEnvironment
|
|
||||||
}
|
|
||||||
environment.desiredIdleRunnersCount = desiredIdleRunnersCount
|
|
||||||
environment.templateJob = newTemplateJob
|
|
||||||
err := nomad.SetMetaConfigValue(newTemplateJob, nomad.ConfigMetaPoolSizeKey,
|
|
||||||
strconv.Itoa(int(desiredIdleRunnersCount)))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("update environment couldn't update template environment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = m.updateRunnerSpecs(id, newTemplateJob)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if scale {
|
|
||||||
err = m.scaleEnvironment(id)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadRunnerManager) updateRunnerSpecs(environmentID EnvironmentID, templateJob *nomadApi.Job) error {
|
|
||||||
runners, err := m.apiClient.LoadRunnerIDs(environmentID.toString())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("update environment couldn't load runners: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var occurredError error
|
|
||||||
for _, id := range runners {
|
|
||||||
// avoid taking the address of the loop variable
|
|
||||||
runnerID := id
|
|
||||||
updatedRunnerJob := *templateJob
|
|
||||||
updatedRunnerJob.ID = &runnerID
|
|
||||||
updatedRunnerJob.Name = &runnerID
|
|
||||||
err := m.apiClient.RegisterRunnerJob(&updatedRunnerJob)
|
|
||||||
if err != nil {
|
|
||||||
if occurredError == nil {
|
|
||||||
occurredError = ErrorUpdatingExecutionEnvironment
|
|
||||||
}
|
|
||||||
occurredError = fmt.Errorf("%w; new api error for runner %s - %v", occurredError, id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return occurredError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadRunnerManager) Claim(environmentID EnvironmentID, duration int) (Runner, error) {
|
|
||||||
environment, ok := m.environments.Get(environmentID)
|
environment, ok := m.environments.Get(environmentID)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrUnknownExecutionEnvironment
|
return nil, ErrUnknownExecutionEnvironment
|
||||||
}
|
}
|
||||||
runner, ok := environment.idleRunners.Sample()
|
runner, ok := environment.Sample(m.apiClient)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrNoRunnersAvailable
|
return nil, ErrNoRunnersAvailable
|
||||||
}
|
}
|
||||||
@ -185,12 +152,6 @@ func (m *NomadRunnerManager) Claim(environmentID EnvironmentID, duration int) (R
|
|||||||
}
|
}
|
||||||
|
|
||||||
runner.SetupTimeout(time.Duration(duration) * time.Second)
|
runner.SetupTimeout(time.Duration(duration) * time.Second)
|
||||||
|
|
||||||
err = m.createRunner(environment)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).WithField("environmentID", environmentID).Error("Couldn't create new runner for claimed one")
|
|
||||||
}
|
|
||||||
|
|
||||||
return runner, nil
|
return runner, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +165,7 @@ func (m *NomadRunnerManager) Get(runnerID string) (Runner, error) {
|
|||||||
|
|
||||||
func (m *NomadRunnerManager) Return(r Runner) error {
|
func (m *NomadRunnerManager) Return(r Runner) error {
|
||||||
r.StopTimeout()
|
r.StopTimeout()
|
||||||
err := m.apiClient.DeleteRunner(r.ID())
|
err := m.apiClient.DeleteJob(r.ID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting runner in Nomad: %w", err)
|
return fmt.Errorf("error deleting runner in Nomad: %w", err)
|
||||||
}
|
}
|
||||||
@ -215,24 +176,23 @@ func (m *NomadRunnerManager) Return(r Runner) error {
|
|||||||
func (m *NomadRunnerManager) Load() {
|
func (m *NomadRunnerManager) Load() {
|
||||||
for _, environment := range m.environments.List() {
|
for _, environment := range m.environments.List() {
|
||||||
environmentLogger := log.WithField("environmentID", environment.ID())
|
environmentLogger := log.WithField("environmentID", environment.ID())
|
||||||
runnerJobs, err := m.apiClient.LoadRunnerJobs(environment.ID().toString())
|
runnerJobs, err := m.apiClient.LoadRunnerJobs(environment.ID().ToString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
environmentLogger.WithError(err).Warn("Error fetching the runner jobs")
|
environmentLogger.WithError(err).Warn("Error fetching the runner jobs")
|
||||||
}
|
}
|
||||||
for _, job := range runnerJobs {
|
for _, job := range runnerJobs {
|
||||||
m.loadSingleJob(job, environmentLogger, environment)
|
m.loadSingleJob(job, environmentLogger, environment)
|
||||||
}
|
}
|
||||||
err = m.scaleEnvironment(environment.ID())
|
err = environment.Scale(m.apiClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
environmentLogger.Error("Couldn't scale environment")
|
environmentLogger.WithError(err).Error("Couldn't scale environment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *NomadRunnerManager) loadSingleJob(job *nomadApi.Job, environmentLogger *logrus.Entry,
|
func (m *NomadRunnerManager) loadSingleJob(job *nomadApi.Job, environmentLogger *logrus.Entry,
|
||||||
environment *NomadEnvironment,
|
environment ExecutionEnvironment) {
|
||||||
) {
|
configTaskGroup := nomad.FindTaskGroup(job, nomad.ConfigTaskGroupName)
|
||||||
configTaskGroup := nomad.FindConfigTaskGroup(job)
|
|
||||||
if configTaskGroup == nil {
|
if configTaskGroup == nil {
|
||||||
environmentLogger.Infof("Couldn't find config task group in job %s, skipping ...", *job.ID)
|
environmentLogger.Infof("Couldn't find config task group in job %s, skipping ...", *job.ID)
|
||||||
return
|
return
|
||||||
@ -253,7 +213,7 @@ func (m *NomadRunnerManager) loadSingleJob(job *nomadApi.Job, environmentLogger
|
|||||||
newJob.SetupTimeout(time.Duration(timeout) * time.Second)
|
newJob.SetupTimeout(time.Duration(timeout) * time.Second)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
environment.idleRunners.Add(newJob)
|
environment.AddRunner(newJob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,137 +230,38 @@ func (m *NomadRunnerManager) keepRunnersSynced(ctx context.Context) {
|
|||||||
func (m *NomadRunnerManager) onAllocationAdded(alloc *nomadApi.Allocation) {
|
func (m *NomadRunnerManager) onAllocationAdded(alloc *nomadApi.Allocation) {
|
||||||
log.WithField("id", alloc.JobID).Debug("Runner started")
|
log.WithField("id", alloc.JobID).Debug("Runner started")
|
||||||
|
|
||||||
if IsEnvironmentTemplateID(alloc.JobID) {
|
if nomad.IsEnvironmentTemplateID(alloc.JobID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
environmentID, err := EnvironmentIDFromJobID(alloc.JobID)
|
environmentID, err := nomad.EnvironmentIDFromRunnerID(alloc.JobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warn("Allocation could not be added")
|
log.WithError(err).Warn("Allocation could not be added")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
job, ok := m.environments.Get(environmentID)
|
environment, ok := m.environments.Get(environmentID)
|
||||||
if ok {
|
if ok {
|
||||||
var mappedPorts []nomadApi.PortMapping
|
var mappedPorts []nomadApi.PortMapping
|
||||||
if alloc.AllocatedResources != nil {
|
if alloc.AllocatedResources != nil {
|
||||||
mappedPorts = alloc.AllocatedResources.Shared.Ports
|
mappedPorts = alloc.AllocatedResources.Shared.Ports
|
||||||
}
|
}
|
||||||
job.idleRunners.Add(NewNomadJob(alloc.JobID, mappedPorts, m.apiClient, m))
|
environment.AddRunner(NewNomadJob(alloc.JobID, mappedPorts, m.apiClient, m))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *NomadRunnerManager) onAllocationStopped(alloc *nomadApi.Allocation) {
|
func (m *NomadRunnerManager) onAllocationStopped(alloc *nomadApi.Allocation) {
|
||||||
log.WithField("id", alloc.JobID).Debug("Runner stopped")
|
log.WithField("id", alloc.JobID).Debug("Runner stopped")
|
||||||
|
|
||||||
environmentID, err := EnvironmentIDFromJobID(alloc.JobID)
|
environmentID, err := nomad.EnvironmentIDFromRunnerID(alloc.JobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warn("Stopped allocation can not be handled")
|
log.WithError(err).Warn("Stopped allocation can not be handled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.usedRunners.Delete(alloc.JobID)
|
m.usedRunners.Delete(alloc.JobID)
|
||||||
job, ok := m.environments.Get(environmentID)
|
environment, ok := m.environments.Get(environmentID)
|
||||||
if ok {
|
if ok {
|
||||||
job.idleRunners.Delete(alloc.JobID)
|
environment.DeleteRunner(alloc.JobID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// scaleEnvironment makes sure that the amount of idle runners is at least the desiredIdleRunnersCount.
|
|
||||||
func (m *NomadRunnerManager) scaleEnvironment(id EnvironmentID) error {
|
|
||||||
environment, ok := m.environments.Get(id)
|
|
||||||
if !ok {
|
|
||||||
return ErrUnknownExecutionEnvironment
|
|
||||||
}
|
|
||||||
|
|
||||||
required := int(environment.desiredIdleRunnersCount) - environment.idleRunners.Length()
|
|
||||||
|
|
||||||
if required > 0 {
|
|
||||||
return m.createRunners(environment, uint(required))
|
|
||||||
} else {
|
|
||||||
return m.removeRunners(environment, uint(-required))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadRunnerManager) createRunners(environment *NomadEnvironment, count uint) error {
|
|
||||||
log.WithField("runnersRequired", count).WithField("id", environment.ID()).Debug("Creating new runners")
|
|
||||||
for i := 0; i < int(count); i++ {
|
|
||||||
err := m.createRunner(environment)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("couldn't create new runner: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadRunnerManager) createRunner(environment *NomadEnvironment) error {
|
|
||||||
newUUID, err := uuid.NewUUID()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed generating runner id: %w", err)
|
|
||||||
}
|
|
||||||
newRunnerID := RunnerJobID(environment.ID(), newUUID.String())
|
|
||||||
|
|
||||||
template := *environment.templateJob
|
|
||||||
template.ID = &newRunnerID
|
|
||||||
template.Name = &newRunnerID
|
|
||||||
|
|
||||||
err = m.apiClient.RegisterRunnerJob(&template)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error registering new runner job: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *NomadRunnerManager) removeRunners(environment *NomadEnvironment, count uint) error {
|
|
||||||
log.WithField("runnersToDelete", count).WithField("id", environment.ID()).Debug("Removing idle runners")
|
|
||||||
for i := 0; i < int(count); i++ {
|
|
||||||
r, ok := environment.idleRunners.Sample()
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("could not delete expected idle runner: %w", ErrRunnerNotFound)
|
|
||||||
}
|
|
||||||
err := m.apiClient.DeleteRunner(r.ID())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not delete expected Nomad idle runner: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunnerJobID returns the nomad job id of the runner with the given environmentID and id.
|
|
||||||
func RunnerJobID(environmentID EnvironmentID, id string) string {
|
|
||||||
return fmt.Sprintf("%d-%s", environmentID, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvironmentIDFromJobID returns the environment id that is part of the passed job id.
|
|
||||||
func EnvironmentIDFromJobID(jobID string) (EnvironmentID, error) {
|
|
||||||
parts := strings.Split(jobID, "-")
|
|
||||||
if len(parts) == 0 {
|
|
||||||
return 0, fmt.Errorf("empty job id: %w", ErrorInvalidJobID)
|
|
||||||
}
|
|
||||||
environmentID, err := strconv.Atoi(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid environment id par %v: %w", err, ErrorInvalidJobID)
|
|
||||||
}
|
|
||||||
return EnvironmentID(environmentID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateJobNameParts = 2
|
|
||||||
|
|
||||||
// TemplateJobID returns the id of the template job for the environment with the given id.
|
|
||||||
func TemplateJobID(id EnvironmentID) string {
|
|
||||||
return fmt.Sprintf("%s-%d", nomad.TemplateJobPrefix, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEnvironmentTemplateID checks if the passed job id belongs to a template job.
|
|
||||||
func IsEnvironmentTemplateID(jobID string) bool {
|
|
||||||
parts := strings.Split(jobID, "-")
|
|
||||||
return len(parts) == templateJobNameParts && parts[0] == nomad.TemplateJobPrefix
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnvironmentIDFromTemplateJobID(id string) (string, error) {
|
|
||||||
parts := strings.Split(id, "-")
|
|
||||||
if len(parts) < templateJobNameParts {
|
|
||||||
return "", fmt.Errorf("invalid template job id: %w", ErrorInvalidJobID)
|
|
||||||
}
|
|
||||||
return parts[1], nil
|
|
||||||
}
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
|
// Code generated by mockery v2.9.4. DO NOT EDIT.
|
||||||
|
|
||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
api "github.com/hashicorp/nomad/api"
|
dto "github.com/openHPI/poseidon/pkg/dto"
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ type ManagerMock struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Claim provides a mock function with given fields: id, duration
|
// Claim provides a mock function with given fields: id, duration
|
||||||
func (_m *ManagerMock) Claim(id EnvironmentID, duration int) (Runner, error) {
|
func (_m *ManagerMock) Claim(id dto.EnvironmentID, duration int) (Runner, error) {
|
||||||
ret := _m.Called(id, duration)
|
ret := _m.Called(id, duration)
|
||||||
|
|
||||||
var r0 Runner
|
var r0 Runner
|
||||||
if rf, ok := ret.Get(0).(func(EnvironmentID, int) Runner); ok {
|
if rf, ok := ret.Get(0).(func(dto.EnvironmentID, int) Runner); ok {
|
||||||
r0 = rf(id, duration)
|
r0 = rf(id, duration)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
@ -26,7 +26,7 @@ func (_m *ManagerMock) Claim(id EnvironmentID, duration int) (Runner, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(EnvironmentID, int) error); ok {
|
if rf, ok := ret.Get(1).(func(dto.EnvironmentID, int) error); ok {
|
||||||
r1 = rf(id, duration)
|
r1 = rf(id, duration)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
@ -35,25 +35,9 @@ func (_m *ManagerMock) Claim(id EnvironmentID, duration int) (Runner, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateOrUpdateEnvironment provides a mock function with given fields: id, desiredIdleRunnersCount, templateJob, scale
|
// DeleteEnvironment provides a mock function with given fields: id
|
||||||
func (_m *ManagerMock) CreateOrUpdateEnvironment(id EnvironmentID, desiredIdleRunnersCount uint, templateJob *api.Job, scale bool) (bool, error) {
|
func (_m *ManagerMock) DeleteEnvironment(id dto.EnvironmentID) {
|
||||||
ret := _m.Called(id, desiredIdleRunnersCount, templateJob, scale)
|
_m.Called(id)
|
||||||
|
|
||||||
var r0 bool
|
|
||||||
if rf, ok := ret.Get(0).(func(EnvironmentID, uint, *api.Job, bool) bool); ok {
|
|
||||||
r0 = rf(id, desiredIdleRunnersCount, templateJob, scale)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(EnvironmentID, uint, *api.Job, bool) error); ok {
|
|
||||||
r1 = rf(id, desiredIdleRunnersCount, templateJob, scale)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get provides a mock function with given fields: runnerID
|
// Get provides a mock function with given fields: runnerID
|
||||||
@ -79,6 +63,45 @@ func (_m *ManagerMock) Get(runnerID string) (Runner, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEnvironment provides a mock function with given fields: id
|
||||||
|
func (_m *ManagerMock) GetEnvironment(id dto.EnvironmentID) (ExecutionEnvironment, bool) {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
var r0 ExecutionEnvironment
|
||||||
|
if rf, ok := ret.Get(0).(func(dto.EnvironmentID) ExecutionEnvironment); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(ExecutionEnvironment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 bool
|
||||||
|
if rf, ok := ret.Get(1).(func(dto.EnvironmentID) bool); ok {
|
||||||
|
r1 = rf(id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Get(1).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnvironments provides a mock function with given fields:
|
||||||
|
func (_m *ManagerMock) ListEnvironments() []ExecutionEnvironment {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 []ExecutionEnvironment
|
||||||
|
if rf, ok := ret.Get(0).(func() []ExecutionEnvironment); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]ExecutionEnvironment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// Load provides a mock function with given fields:
|
// Load provides a mock function with given fields:
|
||||||
func (_m *ManagerMock) Load() {
|
func (_m *ManagerMock) Load() {
|
||||||
_m.Called()
|
_m.Called()
|
||||||
@ -97,3 +120,17 @@ func (_m *ManagerMock) Return(r Runner) error {
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetEnvironment provides a mock function with given fields: environment
|
||||||
|
func (_m *ManagerMock) SetEnvironment(environment ExecutionEnvironment) bool {
|
||||||
|
ret := _m.Called(environment)
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func(ExecutionEnvironment) bool); ok {
|
||||||
|
r0 = rf(environment)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
@ -4,30 +4,26 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
nomadApi "github.com/hashicorp/nomad/api"
|
||||||
"github.com/openHPI/poseidon/internal/nomad"
|
"github.com/openHPI/poseidon/internal/nomad"
|
||||||
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"github.com/openHPI/poseidon/tests"
|
"github.com/openHPI/poseidon/tests"
|
||||||
"github.com/openHPI/poseidon/tests/helpers"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDesiredRunnersCount uint = 5
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetNextRunnerTestSuite(t *testing.T) {
|
func TestGetNextRunnerTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(ManagerTestSuite))
|
suite.Run(t, new(ManagerTestSuite))
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManagerTestSuite struct {
|
type ManagerTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
apiMock *nomad.ExecutorAPIMock
|
apiMock *nomad.ExecutorAPIMock
|
||||||
nomadRunnerManager *NomadRunnerManager
|
nomadRunnerManager *NomadRunnerManager
|
||||||
exerciseRunner Runner
|
exerciseEnvironment *ExecutionEnvironmentMock
|
||||||
|
exerciseRunner Runner
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) SetupTest() {
|
func (s *ManagerTestSuite) SetupTest() {
|
||||||
@ -39,7 +35,8 @@ func (s *ManagerTestSuite) SetupTest() {
|
|||||||
s.nomadRunnerManager = NewNomadRunnerManager(s.apiMock, ctx)
|
s.nomadRunnerManager = NewNomadRunnerManager(s.apiMock, ctx)
|
||||||
|
|
||||||
s.exerciseRunner = NewRunner(tests.DefaultRunnerID, s.nomadRunnerManager)
|
s.exerciseRunner = NewRunner(tests.DefaultRunnerID, s.nomadRunnerManager)
|
||||||
s.registerDefaultEnvironment()
|
s.exerciseEnvironment = &ExecutionEnvironmentMock{}
|
||||||
|
s.setDefaultEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockRunnerQueries(apiMock *nomad.ExecutorAPIMock, returnedRunnerIds []string) {
|
func mockRunnerQueries(apiMock *nomad.ExecutorAPIMock, returnedRunnerIds []string) {
|
||||||
@ -52,44 +49,66 @@ func mockRunnerQueries(apiMock *nomad.ExecutorAPIMock, returnedRunnerIds []strin
|
|||||||
})
|
})
|
||||||
apiMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
apiMock.On("LoadEnvironmentJobs").Return([]*nomadApi.Job{}, nil)
|
||||||
apiMock.On("MarkRunnerAsUsed", mock.AnythingOfType("string"), mock.AnythingOfType("int")).Return(nil)
|
apiMock.On("MarkRunnerAsUsed", mock.AnythingOfType("string"), mock.AnythingOfType("int")).Return(nil)
|
||||||
apiMock.On("LoadRunnerIDs", tests.DefaultJobID).Return(returnedRunnerIds, nil)
|
apiMock.On("LoadRunnerIDs", tests.DefaultRunnerID).Return(returnedRunnerIds, nil)
|
||||||
apiMock.On("JobScale", tests.DefaultJobID).Return(uint(len(returnedRunnerIds)), nil)
|
apiMock.On("JobScale", tests.DefaultRunnerID).Return(uint(len(returnedRunnerIds)), nil)
|
||||||
apiMock.On("SetJobScale", tests.DefaultJobID, mock.AnythingOfType("uint"), "Runner Requested").Return(nil)
|
apiMock.On("SetJobScale", tests.DefaultRunnerID, mock.AnythingOfType("uint"), "Runner Requested").Return(nil)
|
||||||
apiMock.On("RegisterRunnerJob", mock.Anything).Return(nil)
|
apiMock.On("RegisterRunnerJob", mock.Anything).Return(nil)
|
||||||
apiMock.On("MonitorEvaluation", mock.Anything, mock.Anything).Return(nil)
|
apiMock.On("MonitorEvaluation", mock.Anything, mock.Anything).Return(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) registerDefaultEnvironment() {
|
func mockIdleRunners(environmentMock *ExecutionEnvironmentMock) {
|
||||||
err := s.nomadRunnerManager.registerEnvironment(defaultEnvironmentID, 0, &nomadApi.Job{}, true)
|
idleRunner := NewLocalRunnerStorage()
|
||||||
s.Require().NoError(err)
|
environmentMock.On("AddRunner", mock.Anything).Run(func(args mock.Arguments) {
|
||||||
|
r, ok := args.Get(0).(Runner)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idleRunner.Add(r)
|
||||||
|
})
|
||||||
|
sampleCall := environmentMock.On("Sample", mock.Anything)
|
||||||
|
sampleCall.Run(func(args mock.Arguments) {
|
||||||
|
r, ok := idleRunner.Sample()
|
||||||
|
sampleCall.ReturnArguments = mock.Arguments{r, ok}
|
||||||
|
})
|
||||||
|
deleteCall := environmentMock.On("DeleteRunner", mock.AnythingOfType("string"))
|
||||||
|
deleteCall.Run(func(args mock.Arguments) {
|
||||||
|
id, ok := args.Get(0).(string)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idleRunner.Delete(id)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) AddIdleRunnerForDefaultEnvironment(r Runner) {
|
func (s *ManagerTestSuite) setDefaultEnvironment() {
|
||||||
job, _ := s.nomadRunnerManager.environments.Get(defaultEnvironmentID)
|
s.exerciseEnvironment.On("ID").Return(defaultEnvironmentID)
|
||||||
job.idleRunners.Add(r)
|
created := s.nomadRunnerManager.SetEnvironment(s.exerciseEnvironment)
|
||||||
|
s.Require().True(created)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) waitForRunnerRefresh() {
|
func (s *ManagerTestSuite) waitForRunnerRefresh() {
|
||||||
<-time.After(100 * time.Millisecond)
|
<-time.After(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestRegisterEnvironmentAddsNewJob() {
|
func (s *ManagerTestSuite) TestSetEnvironmentAddsNewEnvironment() {
|
||||||
err := s.nomadRunnerManager.
|
anotherEnvironment := &ExecutionEnvironmentMock{}
|
||||||
registerEnvironment(anotherEnvironmentID, defaultDesiredRunnersCount, &nomadApi.Job{}, true)
|
anotherEnvironment.On("ID").Return(anotherEnvironmentID)
|
||||||
s.Require().NoError(err)
|
created := s.nomadRunnerManager.SetEnvironment(anotherEnvironment)
|
||||||
job, ok := s.nomadRunnerManager.environments.Get(defaultEnvironmentID)
|
s.Require().True(created)
|
||||||
|
|
||||||
|
job, ok := s.nomadRunnerManager.environments.Get(anotherEnvironmentID)
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
s.NotNil(job)
|
s.NotNil(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimReturnsNotFoundErrorIfEnvironmentNotFound() {
|
func (s *ManagerTestSuite) TestClaimReturnsNotFoundErrorIfEnvironmentNotFound() {
|
||||||
runner, err := s.nomadRunnerManager.Claim(EnvironmentID(42), defaultInactivityTimeout)
|
runner, err := s.nomadRunnerManager.Claim(anotherEnvironmentID, defaultInactivityTimeout)
|
||||||
s.Nil(runner)
|
s.Nil(runner)
|
||||||
s.Equal(ErrUnknownExecutionEnvironment, err)
|
s.Equal(ErrUnknownExecutionEnvironment, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimReturnsRunnerIfAvailable() {
|
func (s *ManagerTestSuite) TestClaimReturnsRunnerIfAvailable() {
|
||||||
s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner)
|
s.exerciseEnvironment.On("Sample", mock.Anything).Return(s.exerciseRunner, true)
|
||||||
receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
s.Equal(s.exerciseRunner, receivedRunner)
|
s.Equal(s.exerciseRunner, receivedRunner)
|
||||||
@ -97,21 +116,23 @@ func (s *ManagerTestSuite) TestClaimReturnsRunnerIfAvailable() {
|
|||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimReturnsErrorIfNoRunnerAvailable() {
|
func (s *ManagerTestSuite) TestClaimReturnsErrorIfNoRunnerAvailable() {
|
||||||
s.waitForRunnerRefresh()
|
s.waitForRunnerRefresh()
|
||||||
|
s.exerciseEnvironment.On("Sample", mock.Anything).Return(nil, false)
|
||||||
runner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
runner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
||||||
s.Nil(runner)
|
s.Nil(runner)
|
||||||
s.Equal(ErrNoRunnersAvailable, err)
|
s.Equal(ErrNoRunnersAvailable, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimReturnsNoRunnerOfDifferentEnvironment() {
|
func (s *ManagerTestSuite) TestClaimReturnsNoRunnerOfDifferentEnvironment() {
|
||||||
s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner)
|
s.exerciseEnvironment.On("Sample", mock.Anything).Return(s.exerciseRunner, true)
|
||||||
receivedRunner, err := s.nomadRunnerManager.Claim(anotherEnvironmentID, defaultInactivityTimeout)
|
receivedRunner, err := s.nomadRunnerManager.Claim(anotherEnvironmentID, defaultInactivityTimeout)
|
||||||
s.Nil(receivedRunner)
|
s.Nil(receivedRunner)
|
||||||
s.Error(err)
|
s.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimDoesNotReturnTheSameRunnerTwice() {
|
func (s *ManagerTestSuite) TestClaimDoesNotReturnTheSameRunnerTwice() {
|
||||||
s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner)
|
s.exerciseEnvironment.On("Sample", mock.Anything).Return(s.exerciseRunner, true).Once()
|
||||||
s.AddIdleRunnerForDefaultEnvironment(NewRunner(tests.AnotherRunnerID, s.nomadRunnerManager))
|
s.exerciseEnvironment.On("Sample", mock.Anything).
|
||||||
|
Return(NewRunner(tests.AnotherRunnerID, s.nomadRunnerManager), true).Once()
|
||||||
|
|
||||||
firstReceivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
firstReceivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
||||||
s.NoError(err)
|
s.NoError(err)
|
||||||
@ -120,14 +141,8 @@ func (s *ManagerTestSuite) TestClaimDoesNotReturnTheSameRunnerTwice() {
|
|||||||
s.NotEqual(firstReceivedRunner, secondReceivedRunner)
|
s.NotEqual(firstReceivedRunner, secondReceivedRunner)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimThrowsAnErrorIfNoRunnersAvailable() {
|
|
||||||
receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
|
||||||
s.Nil(receivedRunner)
|
|
||||||
s.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestClaimAddsRunnerToUsedRunners() {
|
func (s *ManagerTestSuite) TestClaimAddsRunnerToUsedRunners() {
|
||||||
s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner)
|
s.exerciseEnvironment.On("Sample", mock.Anything).Return(s.exerciseRunner, true)
|
||||||
receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
receivedRunner, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
savedRunner, ok := s.nomadRunnerManager.usedRunners.Get(receivedRunner.ID())
|
savedRunner, ok := s.nomadRunnerManager.usedRunners.Get(receivedRunner.ID())
|
||||||
@ -135,16 +150,6 @@ func (s *ManagerTestSuite) TestClaimAddsRunnerToUsedRunners() {
|
|||||||
s.Equal(savedRunner, receivedRunner)
|
s.Equal(savedRunner, receivedRunner)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestTwoClaimsAddExactlyTwoRunners() {
|
|
||||||
s.AddIdleRunnerForDefaultEnvironment(s.exerciseRunner)
|
|
||||||
s.AddIdleRunnerForDefaultEnvironment(NewRunner(tests.AnotherRunnerID, s.nomadRunnerManager))
|
|
||||||
_, err := s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
_, err = s.nomadRunnerManager.Claim(defaultEnvironmentID, defaultInactivityTimeout)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.apiMock.AssertNumberOfCalls(s.T(), "RegisterRunnerJob", 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestGetReturnsRunnerIfRunnerIsUsed() {
|
func (s *ManagerTestSuite) TestGetReturnsRunnerIfRunnerIsUsed() {
|
||||||
s.nomadRunnerManager.usedRunners.Add(s.exerciseRunner)
|
s.nomadRunnerManager.usedRunners.Add(s.exerciseRunner)
|
||||||
savedRunner, err := s.nomadRunnerManager.Get(s.exerciseRunner.ID())
|
savedRunner, err := s.nomadRunnerManager.Get(s.exerciseRunner.ID())
|
||||||
@ -159,7 +164,7 @@ func (s *ManagerTestSuite) TestGetReturnsErrorIfRunnerNotFound() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestReturnRemovesRunnerFromUsedRunners() {
|
func (s *ManagerTestSuite) TestReturnRemovesRunnerFromUsedRunners() {
|
||||||
s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil)
|
s.apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||||
s.nomadRunnerManager.usedRunners.Add(s.exerciseRunner)
|
s.nomadRunnerManager.usedRunners.Add(s.exerciseRunner)
|
||||||
err := s.nomadRunnerManager.Return(s.exerciseRunner)
|
err := s.nomadRunnerManager.Return(s.exerciseRunner)
|
||||||
s.Nil(err)
|
s.Nil(err)
|
||||||
@ -168,14 +173,14 @@ func (s *ManagerTestSuite) TestReturnRemovesRunnerFromUsedRunners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestReturnCallsDeleteRunnerApiMethod() {
|
func (s *ManagerTestSuite) TestReturnCallsDeleteRunnerApiMethod() {
|
||||||
s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil)
|
s.apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(nil)
|
||||||
err := s.nomadRunnerManager.Return(s.exerciseRunner)
|
err := s.nomadRunnerManager.Return(s.exerciseRunner)
|
||||||
s.Nil(err)
|
s.Nil(err)
|
||||||
s.apiMock.AssertCalled(s.T(), "DeleteRunner", s.exerciseRunner.ID())
|
s.apiMock.AssertCalled(s.T(), "DeleteJob", s.exerciseRunner.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestReturnReturnsErrorWhenApiCallFailed() {
|
func (s *ManagerTestSuite) TestReturnReturnsErrorWhenApiCallFailed() {
|
||||||
s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(tests.ErrDefault)
|
s.apiMock.On("DeleteJob", mock.AnythingOfType("string")).Return(tests.ErrDefault)
|
||||||
err := s.nomadRunnerManager.Return(s.exerciseRunner)
|
err := s.nomadRunnerManager.Return(s.exerciseRunner)
|
||||||
s.Error(err)
|
s.Error(err)
|
||||||
}
|
}
|
||||||
@ -204,9 +209,10 @@ func (s *ManagerTestSuite) TestUpdateRunnersAddsIdleRunner() {
|
|||||||
allocation := &nomadApi.Allocation{ID: tests.DefaultRunnerID}
|
allocation := &nomadApi.Allocation{ID: tests.DefaultRunnerID}
|
||||||
environment, ok := s.nomadRunnerManager.environments.Get(defaultEnvironmentID)
|
environment, ok := s.nomadRunnerManager.environments.Get(defaultEnvironmentID)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
allocation.JobID = environment.environmentID.toString()
|
allocation.JobID = environment.ID().ToString()
|
||||||
|
mockIdleRunners(environment.(*ExecutionEnvironmentMock))
|
||||||
|
|
||||||
_, ok = environment.idleRunners.Get(allocation.ID)
|
_, ok = environment.Sample(s.apiMock)
|
||||||
s.Require().False(ok)
|
s.Require().False(ok)
|
||||||
|
|
||||||
modifyMockedCall(s.apiMock, "WatchAllocations", func(call *mock.Call) {
|
modifyMockedCall(s.apiMock, "WatchAllocations", func(call *mock.Call) {
|
||||||
@ -223,17 +229,18 @@ func (s *ManagerTestSuite) TestUpdateRunnersAddsIdleRunner() {
|
|||||||
go s.nomadRunnerManager.keepRunnersSynced(ctx)
|
go s.nomadRunnerManager.keepRunnersSynced(ctx)
|
||||||
<-time.After(10 * time.Millisecond)
|
<-time.After(10 * time.Millisecond)
|
||||||
|
|
||||||
_, ok = environment.idleRunners.Get(allocation.JobID)
|
_, ok = environment.Sample(s.apiMock)
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestUpdateRunnersRemovesIdleAndUsedRunner() {
|
func (s *ManagerTestSuite) TestUpdateRunnersRemovesIdleAndUsedRunner() {
|
||||||
allocation := &nomadApi.Allocation{JobID: tests.DefaultJobID}
|
allocation := &nomadApi.Allocation{JobID: tests.DefaultRunnerID}
|
||||||
environment, ok := s.nomadRunnerManager.environments.Get(defaultEnvironmentID)
|
environment, ok := s.nomadRunnerManager.environments.Get(defaultEnvironmentID)
|
||||||
s.Require().True(ok)
|
s.Require().True(ok)
|
||||||
|
mockIdleRunners(environment.(*ExecutionEnvironmentMock))
|
||||||
|
|
||||||
testRunner := NewRunner(allocation.JobID, s.nomadRunnerManager)
|
testRunner := NewRunner(allocation.JobID, s.nomadRunnerManager)
|
||||||
environment.idleRunners.Add(testRunner)
|
environment.AddRunner(testRunner)
|
||||||
s.nomadRunnerManager.usedRunners.Add(testRunner)
|
s.nomadRunnerManager.usedRunners.Add(testRunner)
|
||||||
|
|
||||||
modifyMockedCall(s.apiMock, "WatchAllocations", func(call *mock.Call) {
|
modifyMockedCall(s.apiMock, "WatchAllocations", func(call *mock.Call) {
|
||||||
@ -250,33 +257,12 @@ func (s *ManagerTestSuite) TestUpdateRunnersRemovesIdleAndUsedRunner() {
|
|||||||
go s.nomadRunnerManager.keepRunnersSynced(ctx)
|
go s.nomadRunnerManager.keepRunnersSynced(ctx)
|
||||||
<-time.After(10 * time.Millisecond)
|
<-time.After(10 * time.Millisecond)
|
||||||
|
|
||||||
_, ok = environment.idleRunners.Get(allocation.JobID)
|
_, ok = environment.Sample(s.apiMock)
|
||||||
s.False(ok)
|
s.False(ok)
|
||||||
_, ok = s.nomadRunnerManager.usedRunners.Get(allocation.JobID)
|
_, ok = s.nomadRunnerManager.usedRunners.Get(allocation.JobID)
|
||||||
s.False(ok)
|
s.False(ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestUpdateEnvironmentRemovesIdleRunnersWhenScalingDown() {
|
|
||||||
_, job := helpers.CreateTemplateJob()
|
|
||||||
initialRunners := uint(40)
|
|
||||||
updatedRunners := uint(10)
|
|
||||||
err := s.nomadRunnerManager.registerEnvironment(anotherEnvironmentID, initialRunners, job, true)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.apiMock.AssertNumberOfCalls(s.T(), "RegisterRunnerJob", int(initialRunners))
|
|
||||||
environment, ok := s.nomadRunnerManager.environments.Get(anotherEnvironmentID)
|
|
||||||
s.Require().True(ok)
|
|
||||||
for i := 0; i < int(initialRunners); i++ {
|
|
||||||
environment.idleRunners.Add(NewRunner("active-runner-"+strconv.Itoa(i), s.nomadRunnerManager))
|
|
||||||
}
|
|
||||||
|
|
||||||
s.apiMock.On("LoadRunnerIDs", anotherEnvironmentID.toString()).Return([]string{}, nil)
|
|
||||||
s.apiMock.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil)
|
|
||||||
|
|
||||||
err = s.nomadRunnerManager.updateEnvironment(tests.AnotherEnvironmentIDAsInteger, updatedRunners, job, true)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.apiMock.AssertNumberOfCalls(s.T(), "DeleteRunner", int(initialRunners-updatedRunners))
|
|
||||||
}
|
|
||||||
|
|
||||||
func modifyMockedCall(apiMock *nomad.ExecutorAPIMock, method string, modifier func(call *mock.Call)) {
|
func modifyMockedCall(apiMock *nomad.ExecutorAPIMock, method string, modifier func(call *mock.Call)) {
|
||||||
for _, c := range apiMock.ExpectedCalls {
|
for _, c := range apiMock.ExpectedCalls {
|
||||||
if c.Method == method {
|
if c.Method == method {
|
||||||
@ -286,13 +272,16 @@ func modifyMockedCall(apiMock *nomad.ExecutorAPIMock, method string, modifier fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ManagerTestSuite) TestOnAllocationAdded() {
|
func (s *ManagerTestSuite) TestOnAllocationAdded() {
|
||||||
s.registerDefaultEnvironment()
|
|
||||||
s.Run("does not add environment template id job", func() {
|
s.Run("does not add environment template id job", func() {
|
||||||
alloc := &nomadApi.Allocation{JobID: TemplateJobID(tests.DefaultEnvironmentIDAsInteger)}
|
environment, ok := s.nomadRunnerManager.environments.Get(tests.DefaultEnvironmentIDAsInteger)
|
||||||
s.nomadRunnerManager.onAllocationAdded(alloc)
|
|
||||||
job, ok := s.nomadRunnerManager.environments.Get(tests.DefaultEnvironmentIDAsInteger)
|
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
s.Zero(job.idleRunners.Length())
|
mockIdleRunners(environment.(*ExecutionEnvironmentMock))
|
||||||
|
|
||||||
|
alloc := &nomadApi.Allocation{JobID: nomad.TemplateJobID(tests.DefaultEnvironmentIDAsInteger)}
|
||||||
|
s.nomadRunnerManager.onAllocationAdded(alloc)
|
||||||
|
|
||||||
|
_, ok = environment.Sample(s.apiMock)
|
||||||
|
s.False(ok)
|
||||||
})
|
})
|
||||||
s.Run("does not panic when environment id cannot be parsed", func() {
|
s.Run("does not panic when environment id cannot be parsed", func() {
|
||||||
alloc := &nomadApi.Allocation{JobID: ""}
|
alloc := &nomadApi.Allocation{JobID: ""}
|
||||||
@ -301,46 +290,52 @@ func (s *ManagerTestSuite) TestOnAllocationAdded() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
s.Run("does not panic when environment does not exist", func() {
|
s.Run("does not panic when environment does not exist", func() {
|
||||||
nonExistentEnvironment := EnvironmentID(1234)
|
nonExistentEnvironment := dto.EnvironmentID(1234)
|
||||||
_, ok := s.nomadRunnerManager.environments.Get(nonExistentEnvironment)
|
_, ok := s.nomadRunnerManager.environments.Get(nonExistentEnvironment)
|
||||||
s.Require().False(ok)
|
s.Require().False(ok)
|
||||||
|
|
||||||
alloc := &nomadApi.Allocation{JobID: RunnerJobID(nonExistentEnvironment, "1-1-1-1")}
|
alloc := &nomadApi.Allocation{JobID: nomad.RunnerJobID(nonExistentEnvironment, "1-1-1-1")}
|
||||||
s.NotPanics(func() {
|
s.NotPanics(func() {
|
||||||
s.nomadRunnerManager.onAllocationAdded(alloc)
|
s.nomadRunnerManager.onAllocationAdded(alloc)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
s.Run("adds correct job", func() {
|
s.Run("adds correct job", func() {
|
||||||
s.Run("without allocated resources", func() {
|
s.Run("without allocated resources", func() {
|
||||||
|
environment, ok := s.nomadRunnerManager.environments.Get(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
s.True(ok)
|
||||||
|
mockIdleRunners(environment.(*ExecutionEnvironmentMock))
|
||||||
|
|
||||||
alloc := &nomadApi.Allocation{
|
alloc := &nomadApi.Allocation{
|
||||||
JobID: tests.DefaultJobID,
|
JobID: tests.DefaultRunnerID,
|
||||||
AllocatedResources: nil,
|
AllocatedResources: nil,
|
||||||
}
|
}
|
||||||
s.nomadRunnerManager.onAllocationAdded(alloc)
|
s.nomadRunnerManager.onAllocationAdded(alloc)
|
||||||
job, ok := s.nomadRunnerManager.environments.Get(tests.DefaultEnvironmentIDAsInteger)
|
|
||||||
s.True(ok)
|
runner, ok := environment.Sample(s.apiMock)
|
||||||
runner, ok := job.idleRunners.Get(tests.DefaultJobID)
|
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
nomadJob, ok := runner.(*NomadJob)
|
nomadJob, ok := runner.(*NomadJob)
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
s.Equal(nomadJob.id, tests.DefaultJobID)
|
s.Equal(nomadJob.id, tests.DefaultRunnerID)
|
||||||
s.Empty(nomadJob.portMappings)
|
s.Empty(nomadJob.portMappings)
|
||||||
})
|
})
|
||||||
s.Run("with mapped ports", func() {
|
s.Run("with mapped ports", func() {
|
||||||
|
environment, ok := s.nomadRunnerManager.environments.Get(tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
s.True(ok)
|
||||||
|
mockIdleRunners(environment.(*ExecutionEnvironmentMock))
|
||||||
|
|
||||||
alloc := &nomadApi.Allocation{
|
alloc := &nomadApi.Allocation{
|
||||||
JobID: tests.DefaultJobID,
|
JobID: tests.DefaultRunnerID,
|
||||||
AllocatedResources: &nomadApi.AllocatedResources{
|
AllocatedResources: &nomadApi.AllocatedResources{
|
||||||
Shared: nomadApi.AllocatedSharedResources{Ports: tests.DefaultPortMappings},
|
Shared: nomadApi.AllocatedSharedResources{Ports: tests.DefaultPortMappings},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
s.nomadRunnerManager.onAllocationAdded(alloc)
|
s.nomadRunnerManager.onAllocationAdded(alloc)
|
||||||
job, ok := s.nomadRunnerManager.environments.Get(tests.DefaultEnvironmentIDAsInteger)
|
|
||||||
s.True(ok)
|
runner, ok := environment.Sample(s.apiMock)
|
||||||
runner, ok := job.idleRunners.Get(tests.DefaultJobID)
|
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
nomadJob, ok := runner.(*NomadJob)
|
nomadJob, ok := runner.(*NomadJob)
|
||||||
s.True(ok)
|
s.True(ok)
|
||||||
s.Equal(nomadJob.id, tests.DefaultJobID)
|
s.Equal(nomadJob.id, tests.DefaultRunnerID)
|
||||||
s.Equal(nomadJob.portMappings, tests.DefaultPortMappings)
|
s.Equal(nomadJob.portMappings, tests.DefaultPortMappings)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,74 +1,75 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/openHPI/poseidon/pkg/dto"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NomadEnvironmentStorage is an interface for storing Nomad environments.
|
// EnvironmentStorage is an interface for storing environments.
|
||||||
type NomadEnvironmentStorage interface {
|
type EnvironmentStorage interface {
|
||||||
// List returns all environments stored in this storage.
|
// List returns all environments stored in this storage.
|
||||||
List() []*NomadEnvironment
|
List() []ExecutionEnvironment
|
||||||
|
|
||||||
// Add adds an environment to the storage.
|
// Add adds an environment to the storage.
|
||||||
// It overwrites the old environment if one with the same id was already stored.
|
// It overwrites the old environment if one with the same id was already stored.
|
||||||
Add(environment *NomadEnvironment)
|
Add(environment ExecutionEnvironment)
|
||||||
|
|
||||||
// Get returns an environment from the storage.
|
// Get returns an environment from the storage.
|
||||||
// Iff the environment does not exist in the store, ok will be false.
|
// Iff the environment does not exist in the store, ok will be false.
|
||||||
Get(id EnvironmentID) (environment *NomadEnvironment, ok bool)
|
Get(id dto.EnvironmentID) (environment ExecutionEnvironment, ok bool)
|
||||||
|
|
||||||
// Delete deletes the environment with the passed id from the storage. It does nothing if no environment with the id
|
// Delete deletes the environment with the passed id from the storage. It does nothing if no environment with the id
|
||||||
// is present in the storage.
|
// is present in the storage.
|
||||||
Delete(id EnvironmentID)
|
Delete(id dto.EnvironmentID)
|
||||||
|
|
||||||
// Length returns the number of currently stored environments in the storage.
|
// Length returns the number of currently stored environments in the storage.
|
||||||
Length() int
|
Length() int
|
||||||
}
|
}
|
||||||
|
|
||||||
// localNomadEnvironmentStorage stores NomadEnvironment objects in the local application memory.
|
// localEnvironmentStorage stores ExecutionEnvironment objects in the local application memory.
|
||||||
type localNomadEnvironmentStorage struct {
|
type localEnvironmentStorage struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
environments map[EnvironmentID]*NomadEnvironment
|
environments map[dto.EnvironmentID]ExecutionEnvironment
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocalNomadEnvironmentStorage responds with an empty localNomadEnvironmentStorage.
|
// NewLocalEnvironmentStorage responds with an empty localEnvironmentStorage.
|
||||||
// This implementation stores the data thread-safe in the local application memory.
|
// This implementation stores the data thread-safe in the local application memory.
|
||||||
func NewLocalNomadEnvironmentStorage() *localNomadEnvironmentStorage {
|
func NewLocalEnvironmentStorage() *localEnvironmentStorage {
|
||||||
return &localNomadEnvironmentStorage{
|
return &localEnvironmentStorage{
|
||||||
environments: make(map[EnvironmentID]*NomadEnvironment),
|
environments: make(map[dto.EnvironmentID]ExecutionEnvironment),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *localNomadEnvironmentStorage) List() []*NomadEnvironment {
|
func (s *localEnvironmentStorage) List() []ExecutionEnvironment {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
values := make([]*NomadEnvironment, 0, len(s.environments))
|
values := make([]ExecutionEnvironment, 0, len(s.environments))
|
||||||
for _, v := range s.environments {
|
for _, v := range s.environments {
|
||||||
values = append(values, v)
|
values = append(values, v)
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *localNomadEnvironmentStorage) Add(environment *NomadEnvironment) {
|
func (s *localEnvironmentStorage) Add(environment ExecutionEnvironment) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
s.environments[environment.ID()] = environment
|
s.environments[environment.ID()] = environment
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *localNomadEnvironmentStorage) Get(id EnvironmentID) (environment *NomadEnvironment, ok bool) {
|
func (s *localEnvironmentStorage) Get(id dto.EnvironmentID) (environment ExecutionEnvironment, ok bool) {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
environment, ok = s.environments[id]
|
environment, ok = s.environments[id]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *localNomadEnvironmentStorage) Delete(id EnvironmentID) {
|
func (s *localEnvironmentStorage) Delete(id dto.EnvironmentID) {
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer s.Unlock()
|
defer s.Unlock()
|
||||||
delete(s.environments, id)
|
delete(s.environments, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *localNomadEnvironmentStorage) Length() int {
|
func (s *localEnvironmentStorage) Length() int {
|
||||||
s.RLock()
|
s.RLock()
|
||||||
defer s.RUnlock()
|
defer s.RUnlock()
|
||||||
return len(s.environments)
|
return len(s.environments)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
nomadApi "github.com/hashicorp/nomad/api"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -12,13 +11,15 @@ func TestEnvironmentStoreTestSuite(t *testing.T) {
|
|||||||
|
|
||||||
type EnvironmentStoreTestSuite struct {
|
type EnvironmentStoreTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
environmentStorage *localNomadEnvironmentStorage
|
environmentStorage *localEnvironmentStorage
|
||||||
environment *NomadEnvironment
|
environment *ExecutionEnvironmentMock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EnvironmentStoreTestSuite) SetupTest() {
|
func (s *EnvironmentStoreTestSuite) SetupTest() {
|
||||||
s.environmentStorage = NewLocalNomadEnvironmentStorage()
|
s.environmentStorage = NewLocalEnvironmentStorage()
|
||||||
s.environment = &NomadEnvironment{environmentID: defaultEnvironmentID}
|
environmentMock := &ExecutionEnvironmentMock{}
|
||||||
|
environmentMock.On("ID").Return(defaultEnvironmentID)
|
||||||
|
s.environment = environmentMock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EnvironmentStoreTestSuite) TestAddedEnvironmentCanBeRetrieved() {
|
func (s *EnvironmentStoreTestSuite) TestAddedEnvironmentCanBeRetrieved() {
|
||||||
@ -29,8 +30,8 @@ func (s *EnvironmentStoreTestSuite) TestAddedEnvironmentCanBeRetrieved() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *EnvironmentStoreTestSuite) TestEnvironmentWithSameIdOverwritesOldOne() {
|
func (s *EnvironmentStoreTestSuite) TestEnvironmentWithSameIdOverwritesOldOne() {
|
||||||
otherEnvironmentWithSameID := &NomadEnvironment{environmentID: defaultEnvironmentID}
|
otherEnvironmentWithSameID := &ExecutionEnvironmentMock{}
|
||||||
otherEnvironmentWithSameID.templateJob = &nomadApi.Job{}
|
otherEnvironmentWithSameID.On("ID").Return(defaultEnvironmentID)
|
||||||
s.NotEqual(s.environment, otherEnvironmentWithSameID)
|
s.NotEqual(s.environment, otherEnvironmentWithSameID)
|
||||||
|
|
||||||
s.environmentStorage.Add(s.environment)
|
s.environmentStorage.Add(s.environment)
|
||||||
@ -64,7 +65,8 @@ func (s *EnvironmentStoreTestSuite) TestLenChangesOnStoreContentChange() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
s.Run("len increases again when different environment is added", func() {
|
s.Run("len increases again when different environment is added", func() {
|
||||||
anotherEnvironment := &NomadEnvironment{environmentID: anotherEnvironmentID}
|
anotherEnvironment := &ExecutionEnvironmentMock{}
|
||||||
|
anotherEnvironment.On("ID").Return(anotherEnvironmentID)
|
||||||
s.environmentStorage.Add(anotherEnvironment)
|
s.environmentStorage.Add(anotherEnvironment)
|
||||||
s.Equal(2, s.environmentStorage.Length())
|
s.Equal(2, s.environmentStorage.Length())
|
||||||
})
|
})
|
||||||
@ -74,3 +76,28 @@ func (s *EnvironmentStoreTestSuite) TestLenChangesOnStoreContentChange() {
|
|||||||
s.Equal(1, s.environmentStorage.Length())
|
s.Equal(1, s.environmentStorage.Length())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *EnvironmentStoreTestSuite) TestListEnvironments() {
|
||||||
|
s.Run("list returns empty array", func() {
|
||||||
|
environments := s.environmentStorage.List()
|
||||||
|
s.Empty(environments)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("list returns one environment", func() {
|
||||||
|
s.environmentStorage.Add(s.environment)
|
||||||
|
|
||||||
|
environments := s.environmentStorage.List()
|
||||||
|
s.Equal(1, len(environments))
|
||||||
|
s.Equal(defaultEnvironmentID, environments[0].ID())
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("list returns multiple environments", func() {
|
||||||
|
anotherEnvironment := &ExecutionEnvironmentMock{}
|
||||||
|
anotherEnvironment.On("ID").Return(anotherEnvironmentID)
|
||||||
|
s.environmentStorage.Add(s.environment)
|
||||||
|
s.environmentStorage.Add(anotherEnvironment)
|
||||||
|
|
||||||
|
environments := s.environmentStorage.List()
|
||||||
|
s.Equal(2, len(environments))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -25,27 +25,27 @@ import (
|
|||||||
const defaultExecutionID = "execution-id"
|
const defaultExecutionID = "execution-id"
|
||||||
|
|
||||||
func TestIdIsStored(t *testing.T) {
|
func TestIdIsStored(t *testing.T) {
|
||||||
runner := NewNomadJob(tests.DefaultJobID, nil, nil, nil)
|
runner := NewNomadJob(tests.DefaultRunnerID, nil, nil, nil)
|
||||||
assert.Equal(t, tests.DefaultJobID, runner.ID())
|
assert.Equal(t, tests.DefaultRunnerID, runner.ID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMappedPortsAreStoredCorrectly(t *testing.T) {
|
func TestMappedPortsAreStoredCorrectly(t *testing.T) {
|
||||||
runner := NewNomadJob(tests.DefaultJobID, tests.DefaultPortMappings, nil, nil)
|
runner := NewNomadJob(tests.DefaultRunnerID, tests.DefaultPortMappings, nil, nil)
|
||||||
assert.Equal(t, tests.DefaultMappedPorts, runner.MappedPorts())
|
assert.Equal(t, tests.DefaultMappedPorts, runner.MappedPorts())
|
||||||
|
|
||||||
runner = NewNomadJob(tests.DefaultJobID, nil, nil, nil)
|
runner = NewNomadJob(tests.DefaultRunnerID, nil, nil, nil)
|
||||||
assert.Empty(t, runner.MappedPorts())
|
assert.Empty(t, runner.MappedPorts())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalRunner(t *testing.T) {
|
func TestMarshalRunner(t *testing.T) {
|
||||||
runner := NewNomadJob(tests.DefaultJobID, nil, nil, nil)
|
runner := NewNomadJob(tests.DefaultRunnerID, nil, nil, nil)
|
||||||
marshal, err := json.Marshal(runner)
|
marshal, err := json.Marshal(runner)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "{\"runnerId\":\""+tests.DefaultJobID+"\"}", string(marshal))
|
assert.Equal(t, "{\"runnerId\":\""+tests.DefaultRunnerID+"\"}", string(marshal))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecutionRequestIsStored(t *testing.T) {
|
func TestExecutionRequestIsStored(t *testing.T) {
|
||||||
runner := NewNomadJob(tests.DefaultJobID, nil, nil, nil)
|
runner := NewNomadJob(tests.DefaultRunnerID, nil, nil, nil)
|
||||||
executionRequest := &dto.ExecutionRequest{
|
executionRequest := &dto.ExecutionRequest{
|
||||||
Command: "command",
|
Command: "command",
|
||||||
TimeLimit: 10,
|
TimeLimit: 10,
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
// Storage is an interface for storing runners.
|
// Storage is an interface for storing runners.
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
// Add adds an runner to the storage.
|
// Add adds a runner to the storage.
|
||||||
// It overwrites the old runner if one with the same id was already stored.
|
// It overwrites the old runner if one with the same id was already stored.
|
||||||
Add(Runner)
|
Add(Runner)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,6 +32,27 @@ func (er *ExecutionRequest) FullCommand() []string {
|
|||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnvironmentID is an id of an environment.
|
||||||
|
type EnvironmentID int
|
||||||
|
|
||||||
|
// NewEnvironmentID parses a string into an EnvironmentID.
|
||||||
|
func NewEnvironmentID(id string) (EnvironmentID, error) {
|
||||||
|
environment, err := strconv.Atoi(id)
|
||||||
|
return EnvironmentID(environment), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString pareses an EnvironmentID back to a string.
|
||||||
|
func (e EnvironmentID) ToString() string {
|
||||||
|
return strconv.Itoa(int(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutionEnvironmentData is the expected json structure of the response body
|
||||||
|
// for routes returning an execution environment.
|
||||||
|
type ExecutionEnvironmentData struct {
|
||||||
|
ExecutionEnvironmentRequest
|
||||||
|
ID int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
// ExecutionEnvironmentRequest is the expected json structure of the request body
|
// ExecutionEnvironmentRequest is the expected json structure of the request body
|
||||||
// for the create execution environment function.
|
// for the create execution environment function.
|
||||||
type ExecutionEnvironmentRequest struct {
|
type ExecutionEnvironmentRequest struct {
|
||||||
|
@ -20,10 +20,8 @@ const (
|
|||||||
AnotherEnvironmentIDAsString = "42"
|
AnotherEnvironmentIDAsString = "42"
|
||||||
DefaultUUID = "MY-DEFAULT-RANDOM-UUID"
|
DefaultUUID = "MY-DEFAULT-RANDOM-UUID"
|
||||||
AnotherUUID = "another-uuid-43"
|
AnotherUUID = "another-uuid-43"
|
||||||
DefaultJobID = DefaultEnvironmentIDAsString + "-" + DefaultUUID
|
DefaultRunnerID = DefaultEnvironmentIDAsString + "-" + DefaultUUID
|
||||||
AnotherJobID = AnotherEnvironmentIDAsString + "-" + AnotherUUID
|
AnotherRunnerID = AnotherEnvironmentIDAsString + "-" + AnotherUUID
|
||||||
DefaultRunnerID = DefaultJobID
|
|
||||||
AnotherRunnerID = AnotherJobID
|
|
||||||
DefaultExecutionID = "s0m3-3x3cu710n-1d"
|
DefaultExecutionID = "s0m3-3x3cu710n-1d"
|
||||||
DefaultMockID = "m0ck-1d"
|
DefaultMockID = "m0ck-1d"
|
||||||
ShortTimeout = 100 * time.Millisecond
|
ShortTimeout = 100 * time.Millisecond
|
||||||
|
@ -65,7 +65,7 @@ func TestMain(m *testing.M) {
|
|||||||
<-time.After(10 * time.Second)
|
<-time.After(10 * time.Second)
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
cleanupJobsForEnvironment(&testing.T{}, "0")
|
cleanupJobsForEnvironment(&testing.T{}, tests.DefaultEnvironmentIDAsString)
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
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/runner"
|
"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"
|
||||||
"github.com/openHPI/poseidon/tests/helpers"
|
"github.com/openHPI/poseidon/tests/helpers"
|
||||||
@ -65,7 +66,213 @@ func TestCreateOrUpdateEnvironment(t *testing.T) {
|
|||||||
validateJob(t, request)
|
validateJob(t, request)
|
||||||
})
|
})
|
||||||
|
|
||||||
cleanupJobsForEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListEnvironments(t *testing.T) {
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath)
|
||||||
|
|
||||||
|
t.Run("returns list with one element", func(t *testing.T) {
|
||||||
|
response, err := http.Get(path) //nolint:gosec // because we build this path right above
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
||||||
|
environmentsArray := assertEnvironmentArrayInResponse(t, response)
|
||||||
|
assert.Equal(t, 1, len(environmentsArray))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns list including the default environment", func(t *testing.T) {
|
||||||
|
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, 1, 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)
|
||||||
|
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) {
|
||||||
|
// Add environment without Poseidon
|
||||||
|
_, job := helpers.CreateTemplateJob()
|
||||||
|
jobID := nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger)
|
||||||
|
job.ID = &jobID
|
||||||
|
job.Name = &jobID
|
||||||
|
_, _, err := nomadClient.Jobs().Register(job, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// List without fetch should not include the added environment
|
||||||
|
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, 1, len(environmentsArray))
|
||||||
|
assertEnvironment(t, environmentsArray[0], tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
|
||||||
|
// List with fetch should include the added environment
|
||||||
|
response, err = http.Get(path + "?fetch=true") //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)
|
||||||
|
assert.Contains(t, foundIDs, dto.EnvironmentID(tests.AnotherEnvironmentIDAsInteger))
|
||||||
|
})
|
||||||
|
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEnvironment(t *testing.T) {
|
||||||
|
t.Run("returns the default environment", func(t *testing.T) {
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.DefaultEnvironmentIDAsString)
|
||||||
|
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)
|
||||||
|
|
||||||
|
environment := getEnvironmentFromResponse(t, response)
|
||||||
|
assertEnvironment(t, environment, tests.DefaultEnvironmentIDAsInteger)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Added environments can be retrieved without fetch", func(t *testing.T) {
|
||||||
|
createEnvironment(t, 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
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||||
|
|
||||||
|
environment := getEnvironmentFromResponse(t, response)
|
||||||
|
assertEnvironment(t, environment, tests.AnotherEnvironmentIDAsInteger)
|
||||||
|
})
|
||||||
|
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
||||||
|
|
||||||
|
t.Run("Added environments can be retrieved with fetch", func(t *testing.T) {
|
||||||
|
// Add environment without Poseidon
|
||||||
|
_, job := helpers.CreateTemplateJob()
|
||||||
|
jobID := nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger)
|
||||||
|
job.ID = &jobID
|
||||||
|
job.Name = &jobID
|
||||||
|
_, _, err := nomadClient.Jobs().Register(job, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// List without fetch should not include the added environment
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString)
|
||||||
|
response, err := http.Get(path) //nolint:gosec // because we build this path right above
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusNotFound, response.StatusCode)
|
||||||
|
|
||||||
|
// List with fetch should include the added environment
|
||||||
|
response, err = http.Get(path + "?fetch=true") //nolint:gosec // because we build this path right above
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||||
|
environment := getEnvironmentFromResponse(t, response)
|
||||||
|
assertEnvironment(t, environment, tests.AnotherEnvironmentIDAsInteger)
|
||||||
|
})
|
||||||
|
deleteEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteEnvironment(t *testing.T) {
|
||||||
|
t.Run("Removes added environment", func(t *testing.T) {
|
||||||
|
createEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
||||||
|
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString)
|
||||||
|
response, err := helpers.HTTPDelete(path, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusNoContent, response.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Removes Nomad Job", func(t *testing.T) {
|
||||||
|
createEnvironment(t, tests.AnotherEnvironmentIDAsString)
|
||||||
|
|
||||||
|
// Expect created Nomad job
|
||||||
|
jobID := nomad.TemplateJobID(tests.AnotherEnvironmentIDAsInteger)
|
||||||
|
job, _, err := nomadClient.Jobs().Info(jobID, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, jobID, *job.ID)
|
||||||
|
|
||||||
|
// Delete the job
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, tests.AnotherEnvironmentIDAsString)
|
||||||
|
response, err := helpers.HTTPDelete(path, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, http.StatusNoContent, response.StatusCode)
|
||||||
|
|
||||||
|
// Expect not to find the Nomad job
|
||||||
|
_, _, err = nomadClient.Jobs().Info(jobID, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIDsFromEnvironments(t *testing.T, environments []interface{}) (ids []dto.EnvironmentID) {
|
||||||
|
t.Helper()
|
||||||
|
for _, environment := range environments {
|
||||||
|
id, _ := parseEnvironment(t, environment)
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEnvironment(t *testing.T, environment interface{}, expectedID dto.EnvironmentID) {
|
||||||
|
t.Helper()
|
||||||
|
id, defaultEnvironmentParams := parseEnvironment(t, environment)
|
||||||
|
|
||||||
|
assert.Equal(t, expectedID, id)
|
||||||
|
expectedKeys := []string{"prewarmingPoolSize", "cpuLimit", "memoryLimit", "image", "networkAccess", "exposedPorts"}
|
||||||
|
for _, key := range expectedKeys {
|
||||||
|
_, ok := defaultEnvironmentParams[key]
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvironment(t *testing.T, environment interface{}) (id dto.EnvironmentID, params map[string]interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
environmentParams, ok := environment.(map[string]interface{})
|
||||||
|
require.True(t, ok)
|
||||||
|
idInterface, ok := environmentParams["id"]
|
||||||
|
require.True(t, ok)
|
||||||
|
idFloat, ok := idInterface.(float64)
|
||||||
|
require.True(t, ok)
|
||||||
|
return dto.EnvironmentID(int(idFloat)), environmentParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEnvironmentArrayInResponse(t *testing.T, response *http.Response) []interface{} {
|
||||||
|
t.Helper()
|
||||||
|
paramMap := make(map[string]interface{})
|
||||||
|
err := json.NewDecoder(response.Body).Decode(¶mMap)
|
||||||
|
require.NoError(t, err)
|
||||||
|
environments, ok := paramMap["executionEnvironments"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
environmentsArray, ok := environments.([]interface{})
|
||||||
|
assert.True(t, ok)
|
||||||
|
return environmentsArray
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvironmentFromResponse(t *testing.T, response *http.Response) interface{} {
|
||||||
|
t.Helper()
|
||||||
|
var environment interface{}
|
||||||
|
err := json.NewDecoder(response.Body).Decode(&environment)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return environment
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:unparam // Because its more clear if the environment id is written in the real test
|
||||||
|
func deleteEnvironment(t *testing.T, id string) {
|
||||||
|
t.Helper()
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, id)
|
||||||
|
_, err := helpers.HTTPDelete(path, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupJobsForEnvironment(t *testing.T, environmentID string) {
|
func cleanupJobsForEnvironment(t *testing.T, environmentID string) {
|
||||||
@ -84,6 +291,21 @@ func cleanupJobsForEnvironment(t *testing.T, environmentID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:unparam // Because its more clear if the environment id is written in the real test
|
||||||
|
func createEnvironment(t *testing.T, environmentID string) {
|
||||||
|
t.Helper()
|
||||||
|
path := helpers.BuildURL(api.BasePath, api.EnvironmentsPath, environmentID)
|
||||||
|
request := dto.ExecutionEnvironmentRequest{
|
||||||
|
PrewarmingPoolSize: 1,
|
||||||
|
CPULimit: 100,
|
||||||
|
MemoryLimit: 100,
|
||||||
|
Image: *testDockerImage,
|
||||||
|
NetworkAccess: false,
|
||||||
|
ExposedPorts: nil,
|
||||||
|
}
|
||||||
|
assertPutReturnsStatusAndZeroContent(t, path, request, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
func assertPutReturnsStatusAndZeroContent(t *testing.T, path string,
|
func assertPutReturnsStatusAndZeroContent(t *testing.T, path string,
|
||||||
request dto.ExecutionEnvironmentRequest, status int) {
|
request dto.ExecutionEnvironmentRequest, status int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@ -133,9 +355,9 @@ func validateJob(t *testing.T, expected dto.ExecutionEnvironmentRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func findTemplateJob(t *testing.T, id runner.EnvironmentID) *nomadApi.Job {
|
func findTemplateJob(t *testing.T, id dto.EnvironmentID) *nomadApi.Job {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
job, _, err := nomadClient.Jobs().Info(runner.TemplateJobID(id), nil)
|
job, _, err := nomadClient.Jobs().Info(nomad.TemplateJobID(id), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error retrieving Nomad job: %v", err)
|
t.Fatalf("Error retrieving Nomad job: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -174,8 +174,9 @@ func HTTPPutJSON(url string, body interface{}) (response *http.Response, err err
|
|||||||
const templateJobPriority = 100
|
const templateJobPriority = 100
|
||||||
|
|
||||||
func CreateTemplateJob() (base, job *nomadApi.Job) {
|
func CreateTemplateJob() (base, job *nomadApi.Job) {
|
||||||
base = nomadApi.NewBatchJob(tests.DefaultJobID, tests.DefaultJobID, "region-name", templateJobPriority)
|
base = nomadApi.NewBatchJob(tests.DefaultRunnerID, tests.DefaultRunnerID, "global", templateJobPriority)
|
||||||
job = nomadApi.NewBatchJob(tests.DefaultJobID, tests.DefaultJobID, "region-name", templateJobPriority)
|
job = nomadApi.NewBatchJob(tests.DefaultRunnerID, tests.DefaultRunnerID, "global", templateJobPriority)
|
||||||
|
job.Datacenters = []string{"dc1"}
|
||||||
configTaskGroup := nomadApi.NewTaskGroup("config", 0)
|
configTaskGroup := nomadApi.NewTaskGroup("config", 0)
|
||||||
configTaskGroup.Meta = make(map[string]string)
|
configTaskGroup.Meta = make(map[string]string)
|
||||||
configTaskGroup.Meta["prewarmingPoolSize"] = "0"
|
configTaskGroup.Meta["prewarmingPoolSize"] = "0"
|
||||||
|
Reference in New Issue
Block a user