Add destroy runner route
This commit is contained in:
12
api/api.go
12
api/api.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/auth"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/auth"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/logging"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/logging"
|
||||||
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/nomad"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,18 +22,17 @@ const (
|
|||||||
// always returns a router for the newest version of our API. We
|
// always returns a router for the newest version of our API. We
|
||||||
// use gorilla/mux because it is more convenient than net/http, e.g.
|
// use gorilla/mux because it is more convenient than net/http, e.g.
|
||||||
// when extracting path parameters.
|
// when extracting path parameters.
|
||||||
func NewRouter(runnerPool environment.RunnerPool) *mux.Router {
|
func NewRouter(apiClient nomad.ExecutorApi, runnerPool environment.RunnerPool) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
// this can later be restricted to a specific host with
|
// this can later be restricted to a specific host with
|
||||||
// `router.Host(...)` and to HTTPS with `router.Schemes("https")`
|
// `router.Host(...)` and to HTTPS with `router.Schemes("https")`
|
||||||
router = newRouterV1(router, runnerPool)
|
router = newRouterV1(router, apiClient, runnerPool)
|
||||||
router.Use(logging.HTTPLoggingMiddleware)
|
router.Use(logging.HTTPLoggingMiddleware)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRouterV1 returns a sub-router containing the routes of version
|
// newRouterV1 returns a sub-router containing the routes of version 1 of our API.
|
||||||
// 1 of our API.
|
func newRouterV1(router *mux.Router, apiClient nomad.ExecutorApi, runnerPool environment.RunnerPool) *mux.Router {
|
||||||
func newRouterV1(router *mux.Router, runnerPool environment.RunnerPool) *mux.Router {
|
|
||||||
v1 := router.PathPrefix(RouteBase).Subrouter()
|
v1 := router.PathPrefix(RouteBase).Subrouter()
|
||||||
v1.HandleFunc(RouteHealth, Health).Methods(http.MethodGet)
|
v1.HandleFunc(RouteHealth, Health).Methods(http.MethodGet)
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ func newRouterV1(router *mux.Router, runnerPool environment.RunnerPool) *mux.Rou
|
|||||||
v1 = v1.PathPrefix("").Subrouter()
|
v1 = v1.PathPrefix("").Subrouter()
|
||||||
v1.Use(auth.HTTPAuthenticationMiddleware)
|
v1.Use(auth.HTTPAuthenticationMiddleware)
|
||||||
}
|
}
|
||||||
registerRunnerRoutes(v1.PathPrefix(RouteRunners).Subrouter(), runnerPool)
|
registerRunnerRoutes(v1.PathPrefix(RouteRunners).Subrouter(), apiClient, runnerPool)
|
||||||
|
|
||||||
return v1
|
return v1
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ func mockHTTPHandler(writer http.ResponseWriter, _ *http.Request) {
|
|||||||
func TestNewRouterV1WithAuthenticationDisabled(t *testing.T) {
|
func TestNewRouterV1WithAuthenticationDisabled(t *testing.T) {
|
||||||
config.Config.Server.Token = ""
|
config.Config.Server.Token = ""
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
v1 := newRouterV1(router, environment.NewLocalRunnerPool())
|
v1 := newRouterV1(router, nil, environment.NewLocalRunnerPool())
|
||||||
|
|
||||||
t.Run("health route is accessible", func(t *testing.T) {
|
t.Run("health route is accessible", func(t *testing.T) {
|
||||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
||||||
@@ -44,7 +44,7 @@ func TestNewRouterV1WithAuthenticationDisabled(t *testing.T) {
|
|||||||
func TestNewRouterV1WithAuthenticationEnabled(t *testing.T) {
|
func TestNewRouterV1WithAuthenticationEnabled(t *testing.T) {
|
||||||
config.Config.Server.Token = "TestToken"
|
config.Config.Server.Token = "TestToken"
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
v1 := newRouterV1(router, environment.NewLocalRunnerPool())
|
v1 := newRouterV1(router, nil, environment.NewLocalRunnerPool())
|
||||||
|
|
||||||
t.Run("health route is accessible", func(t *testing.T) {
|
t.Run("health route is accessible", func(t *testing.T) {
|
||||||
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
request, err := http.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/config"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/config"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
|
||||||
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/nomad"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/runner"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/runner"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
ExecutePath = "/execute"
|
ExecutePath = "/execute"
|
||||||
WebsocketPath = "/websocket"
|
WebsocketPath = "/websocket"
|
||||||
|
DeleteRoute = "deleteRunner"
|
||||||
RunnerIdKey = "runnerId"
|
RunnerIdKey = "runnerId"
|
||||||
ExecutionIdKey = "executionId"
|
ExecutionIdKey = "executionId"
|
||||||
)
|
)
|
||||||
@@ -111,10 +113,27 @@ func findRunnerMiddleware(runnerPool environment.RunnerPool) func(handler http.H
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRunnerRoutes(router *mux.Router, runnerPool environment.RunnerPool) {
|
func deleteRunner(apiClient nomad.ExecutorApi, runnerPool environment.RunnerPool) func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
targetRunner, _ := runner.FromContext(request.Context())
|
||||||
|
|
||||||
|
err := apiClient.DeleteRunner(targetRunner.Id())
|
||||||
|
if err != nil {
|
||||||
|
writeInternalServerError(writer, err, dto.ErrorNomadInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runnerPool.Delete(targetRunner.Id())
|
||||||
|
|
||||||
|
writer.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRunnerRoutes(router *mux.Router, apiClient nomad.ExecutorApi, runnerPool environment.RunnerPool) {
|
||||||
router.HandleFunc("", provideRunner).Methods(http.MethodPost)
|
router.HandleFunc("", provideRunner).Methods(http.MethodPost)
|
||||||
runnerRouter := router.PathPrefix(fmt.Sprintf("/{%s}", RunnerIdKey)).Subrouter()
|
runnerRouter := router.PathPrefix(fmt.Sprintf("/{%s}", RunnerIdKey)).Subrouter()
|
||||||
runnerRouter.Use(findRunnerMiddleware(runnerPool))
|
runnerRouter.Use(findRunnerMiddleware(runnerPool))
|
||||||
runnerRouter.HandleFunc(ExecutePath, executeCommand(runnerRouter)).Methods(http.MethodPost).Name(ExecutePath)
|
runnerRouter.HandleFunc(ExecutePath, executeCommand(runnerRouter)).Methods(http.MethodPost).Name(ExecutePath)
|
||||||
runnerRouter.HandleFunc(WebsocketPath, connectToRunner).Methods(http.MethodGet).Name(WebsocketPath)
|
runnerRouter.HandleFunc(WebsocketPath, connectToRunner).Methods(http.MethodGet).Name(WebsocketPath)
|
||||||
|
runnerRouter.HandleFunc("", deleteRunner(apiClient, runnerPool)).Methods(http.MethodDelete).Name(DeleteRoute)
|
||||||
}
|
}
|
||||||
|
@@ -3,11 +3,15 @@ package api
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/api/dto"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/environment"
|
||||||
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/mocks"
|
||||||
"gitlab.hpi.de/codeocean/codemoon/poseidon/runner"
|
"gitlab.hpi.de/codeocean/codemoon/poseidon/runner"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -67,7 +71,7 @@ func TestFindRunnerMiddleware(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRoute(t *testing.T) {
|
func TestExecuteRoute(t *testing.T) {
|
||||||
runnerPool := environment.NewLocalRunnerPool()
|
runnerPool := environment.NewLocalRunnerPool()
|
||||||
router := NewRouter(runnerPool)
|
router := NewRouter(nil, runnerPool)
|
||||||
testRunner := runner.NewExerciseRunner("testRunner")
|
testRunner := runner.NewExerciseRunner("testRunner")
|
||||||
runnerPool.Add(testRunner)
|
runnerPool.Add(testRunner)
|
||||||
|
|
||||||
@@ -127,3 +131,89 @@ func TestExecuteRoute(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteRunnerRouteTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(DeleteRunnerRouteTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteRunnerRouteTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
runnerPool environment.RunnerPool
|
||||||
|
apiClient *mocks.ExecutorApi
|
||||||
|
router *mux.Router
|
||||||
|
testRunner runner.Runner
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DeleteRunnerRouteTestSuite) SetupTest() {
|
||||||
|
suite.runnerPool = environment.NewLocalRunnerPool()
|
||||||
|
suite.apiClient = &mocks.ExecutorApi{}
|
||||||
|
suite.router = NewRouter(suite.apiClient, suite.runnerPool)
|
||||||
|
|
||||||
|
suite.testRunner = runner.NewExerciseRunner("testRunner")
|
||||||
|
suite.runnerPool.Add(suite.testRunner)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
runnerUrl, err := suite.router.Get(DeleteRoute).URL(RunnerIdKey, suite.testRunner.Id())
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Fatal(err)
|
||||||
|
}
|
||||||
|
suite.path = runnerUrl.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DeleteRunnerRouteTestSuite) TestValidRequestReturnsNoContent() {
|
||||||
|
suite.apiClient.On("DeleteRunner", mock.AnythingOfType("string")).Return(nil)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request, err := http.NewRequest(http.MethodDelete, suite.path, nil)
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusNoContent, recorder.Code)
|
||||||
|
|
||||||
|
suite.Run("runner is deleted on nomad", func() {
|
||||||
|
suite.apiClient.AssertCalled(suite.T(), "DeleteRunner", suite.testRunner.Id())
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Run("runner is deleted from runnerPool", func() {
|
||||||
|
returnedRunner, ok := suite.runnerPool.Get(suite.testRunner.Id())
|
||||||
|
suite.Nil(returnedRunner)
|
||||||
|
suite.False(ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DeleteRunnerRouteTestSuite) TestReturnInternalServerErrorWhenApiCallToNomadFailed() {
|
||||||
|
suite.apiClient.On("DeleteRunner", mock.AnythingOfType("string")).Return(errors.New("API call failed"))
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request, err := http.NewRequest(http.MethodDelete, suite.path, nil)
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusInternalServerError, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *DeleteRunnerRouteTestSuite) TestDeleteInvalidRunnerIdReturnsNotFound() {
|
||||||
|
var err error
|
||||||
|
runnersUrl, err := suite.router.Get(DeleteRoute).URL(RunnerIdKey, "1nv4l1dID")
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Fatal(err)
|
||||||
|
}
|
||||||
|
suite.path = runnersUrl.String()
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request, err := http.NewRequest(http.MethodDelete, suite.path, nil)
|
||||||
|
if err != nil {
|
||||||
|
suite.T().Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusNotFound, recorder.Code)
|
||||||
|
}
|
||||||
|
6
main.go
6
main.go
@@ -38,13 +38,13 @@ func runServer(server *http.Server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initServer(runnerPool environment.RunnerPool) *http.Server {
|
func initServer(apiClient nomad.ExecutorApi, runnerPool environment.RunnerPool) *http.Server {
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
Addr: config.Config.PoseidonAPIURL().Host,
|
Addr: config.Config.PoseidonAPIURL().Host,
|
||||||
WriteTimeout: time.Second * 15,
|
WriteTimeout: time.Second * 15,
|
||||||
ReadTimeout: time.Second * 15,
|
ReadTimeout: time.Second * 15,
|
||||||
IdleTimeout: time.Second * 60,
|
IdleTimeout: time.Second * 60,
|
||||||
Handler: api.NewRouter(runnerPool),
|
Handler: api.NewRouter(apiClient, runnerPool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ func main() {
|
|||||||
runnerPool := environment.NewLocalRunnerPool()
|
runnerPool := environment.NewLocalRunnerPool()
|
||||||
environment.DebugInit(runnerPool, nomadAPIClient)
|
environment.DebugInit(runnerPool, nomadAPIClient)
|
||||||
|
|
||||||
server := initServer(runnerPool)
|
server := initServer(nomadAPIClient, runnerPool)
|
||||||
go runServer(server)
|
go runServer(server)
|
||||||
shutdownOnOSSignal(server)
|
shutdownOnOSSignal(server)
|
||||||
}
|
}
|
||||||
|
@@ -12,9 +12,18 @@ type ExecutorApi struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateJob provides a mock function with given fields:
|
// DeleteRunner provides a mock function with given fields: runnerId
|
||||||
func (_m *ExecutorApi) CreateJob() {
|
func (_m *ExecutorApi) DeleteRunner(runnerId string) error {
|
||||||
_m.Called()
|
ret := _m.Called(runnerId)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(runnerId)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJobScale provides a mock function with given fields: jobId
|
// GetJobScale provides a mock function with given fields: jobId
|
||||||
|
@@ -11,6 +11,7 @@ type ExecutorApi interface {
|
|||||||
GetJobScale(jobId string) (jobScale int, err error)
|
GetJobScale(jobId string) (jobScale int, err error)
|
||||||
SetJobScaling(jobId string, count int, reason string) (err error)
|
SetJobScaling(jobId string, count int, reason string) (err error)
|
||||||
LoadRunners(jobId string) (runnerIds []string, err error)
|
LoadRunners(jobId string) (runnerIds []string, err error)
|
||||||
|
DeleteRunner(runnerId string) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApiClient provides access to the Nomad functionality
|
// ApiClient provides access to the Nomad functionality
|
||||||
@@ -69,3 +70,12 @@ func (apiClient *ApiClient) LoadRunners(jobId string) (runnerIds []string, err e
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (apiClient *ApiClient) DeleteRunner(runnerId string) (err error) {
|
||||||
|
allocation, _, err := apiClient.client.Allocations().Info(runnerId, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = apiClient.client.Allocations().Stop(allocation, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user