Add destroy runner route

This commit is contained in:
Jan-Eric Hellenberg
2021-05-05 09:07:23 +02:00
parent e45cd92557
commit ba51956ec3
7 changed files with 144 additions and 16 deletions

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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)
}

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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
}