Add basic nomad integration and runner provider

This commit is contained in:
Maximilian Paß
2021-05-04 13:04:37 +02:00
parent 40a22ecbb1
commit c092199693
24 changed files with 1141 additions and 34 deletions

View File

@ -3,12 +3,19 @@ package api
import (
"encoding/json"
"github.com/gorilla/mux"
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
"gitlab.hpi.de/codeocean/codemoon/poseidon/logging"
"net/http"
)
var log = logging.GetLogger("api")
const (
RouteBase = "/api/v1"
RouteHealth = "/health"
RouteRunners = "/runners"
)
// NewRouter returns an HTTP handler (http.Handler) which can be
// used by the net/http package to serve the routes of our API. It
// always returns a router for the newest version of our API. We
@ -23,20 +30,34 @@ func NewRouter() http.Handler {
return router
}
// newRouterV1 returns a subrouter containing the routes of version
// newRouterV1 returns a sub-router containing the routes of version
// 1 of our API.
func newRouterV1(router *mux.Router) *mux.Router {
v1 := router.PathPrefix("/api/v1").Subrouter()
v1.HandleFunc("/health", Health).Methods(http.MethodGet)
v1 := router.PathPrefix(RouteBase).Subrouter()
v1.HandleFunc(RouteHealth, Health).Methods(http.MethodGet)
registerRunnerRoutes(v1.PathPrefix(RouteRunners).Subrouter())
return v1
}
func writeJson(writer http.ResponseWriter, content interface{}) {
func writeInternalServerError(writer http.ResponseWriter, err error, errorCode dto.ErrorCode) {
sendJson(writer, &dto.InternalServerError{Message: err.Error(), ErrorCode: errorCode}, http.StatusInternalServerError)
}
func writeBadRequest(writer http.ResponseWriter, err error) {
sendJson(writer, &dto.ClientError{Message: err.Error()}, http.StatusBadRequest)
}
func writeNotFound(writer http.ResponseWriter, err error) {
sendJson(writer, &dto.ClientError{Message: err.Error()}, http.StatusNotFound)
}
func sendJson(writer http.ResponseWriter, content interface{}, httpStatusCode int) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(httpStatusCode)
response, err := json.Marshal(content)
if err != nil {
log.WithError(err).Warn("JSON marshal error")
http.Error(writer, err.Error(), http.StatusInternalServerError)
// cannot produce infinite recursive loop, since json.Marshal of dto.InternalServerError won't return an error
writeInternalServerError(writer, err, dto.ErrorUnknown)
return
}
if _, err := writer.Write(response); err != nil {

33
api/dto/dto.go Normal file
View File

@ -0,0 +1,33 @@
package dto
// RequestRunner is the expected json structure of the request body for the ProvideRunner function
type RequestRunner struct {
ExecutionEnvironmentId int `json:"executionEnvironmentId"`
InactivityTimeout int `json:"inactivityTimeout"`
}
// ResponseRunner is the expected result from the api server
type ResponseRunner struct {
Id string `json:"runnerId"`
}
// ClientError is the response interface if the request is not valid
type ClientError struct {
Message string `json:"message"`
}
// InternalServerError is the response interface that is returned when an error occurs
type InternalServerError struct {
Message string `json:"message"`
ErrorCode ErrorCode `json:"errorCode"`
}
// ErrorCode is the type for error codes expected by CodeOcean
type ErrorCode string
const (
ErrorNomadUnreachable ErrorCode = "NOMAD_UNREACHABLE"
ErrorNomadOverload ErrorCode = "NOMAD_OVERLOAD"
ErrorNomadInternalServerError ErrorCode = "NOMAD_INTERNAL_SERVER_ERROR"
ErrorUnknown ErrorCode = "UNKNOWN"
)

View File

@ -7,8 +7,5 @@ import (
// Health tries to respond that the server is alive.
// If it is not, the response won't reach the client.
func Health(writer http.ResponseWriter, _ *http.Request) {
writeJson(
writer,
Message{"I'm alive!"},
)
writer.WriteHeader(http.StatusNoContent)
}

View File

@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
@ -15,10 +14,5 @@ func TestHealthRoute(t *testing.T) {
}
recorder := httptest.NewRecorder()
http.HandlerFunc(Health).ServeHTTP(recorder, request)
result := Message{}
_ = json.Unmarshal(recorder.Body.Bytes(), &result)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
assert.Equal(t, "I'm alive!", result.Message)
assert.Equal(t, http.StatusNoContent, recorder.Code)
}

View File

@ -1,5 +0,0 @@
package api
type Message struct {
Message string `json:"msg"`
}

35
api/runners.go Normal file
View File

@ -0,0 +1,35 @@
package api
import (
"encoding/json"
"github.com/gorilla/mux"
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
"net/http"
)
// ProvideRunner tries to respond with the id of a runner.
// This runner is then reserved for future use.
func ProvideRunner(writer http.ResponseWriter, request *http.Request) {
runnerRequest := new(dto.RequestRunner)
if err := json.NewDecoder(request.Body).Decode(runnerRequest); err != nil {
writeBadRequest(writer, err)
return
}
environment, err := environment.GetExecutionEnvironment(runnerRequest.ExecutionEnvironmentId)
if err != nil {
writeNotFound(writer, err)
return
}
runner, err := environment.NextRunner()
if err != nil {
writeInternalServerError(writer, err, dto.ErrorNomadOverload)
return
}
sendJson(writer, &dto.ResponseRunner{Id: runner.Id()}, http.StatusOK)
}
func registerRunnerRoutes(router *mux.Router) {
router.HandleFunc("", ProvideRunner).Methods(http.MethodPost)
}