From f471879984435f6c258315a8ac678937669509d8 Mon Sep 17 00:00:00 2001 From: Max Siegieda Date: Mon, 12 Nov 2018 11:57:42 +0000 Subject: [PATCH] add report handlers and routes to Simulations --- handlers/api/simulation.go | 79 ++++++++++++++++++++++++++++++++- handlers/api/simulation_test.go | 49 +++++++++++++++++++- main.go | 3 +- routes/routes.go | 6 ++- routes/routes_test.go | 2 +- 5 files changed, 134 insertions(+), 5 deletions(-) diff --git a/handlers/api/simulation.go b/handlers/api/simulation.go index 30ef560b..039d0481 100644 --- a/handlers/api/simulation.go +++ b/handlers/api/simulation.go @@ -1,6 +1,8 @@ package api import ( + "crypto/subtle" + "errors" "fmt" "github.com/ReconfigureIO/platform/middleware" @@ -19,14 +21,16 @@ type Simulation struct { AWS batch.Service Events events.EventService Storage storage.Service + Repo models.SimulationRepo } // NewSimulation creates a new Simulation. -func NewSimulation(events events.EventService, storageService storage.Service, awsSession batch.Service) Simulation { +func NewSimulation(events events.EventService, storageService storage.Service, awsSession batch.Service, repo models.SimulationRepo) Simulation { return Simulation{ AWS: awsSession, Events: events, Storage: storageService, + Repo: repo, } } @@ -192,6 +196,15 @@ func (s Simulation) canPostEvent(c *gin.Context, sim models.Simulation) bool { return false } +// isTokenAuthorized handles authentication and authorization for workers. On a +// job's (e.g. simulation) creation it is given a token which is also given to +// the worker that processes the job. When the worker sends events or reports to +// the API it includes this token in the request. +func isTokenAuthorized(c *gin.Context, correctToken string) bool { + gotToken, ok := c.GetQuery("token") + return ok && subtle.ConstantTimeCompare([]byte(gotToken), []byte(correctToken)) == 1 +} + // CreateEvent creates a new event. func (s Simulation) CreateEvent(c *gin.Context) { sim, err := s.unauthOne(c) @@ -221,6 +234,7 @@ func (s Simulation) CreateEvent(c *gin.Context) { _, isUser := middleware.CheckUser(c) if event.Status == models.StatusTerminated && isUser { sugar.ErrResponse(c, 400, fmt.Sprintf("Users cannot post TERMINATED events, please upgrade to reco v0.3.1 or above")) + return } newEvent, err := BatchService{AWS: s.AWS}.AddEvent(&sim.BatchJob, event) @@ -235,3 +249,66 @@ func (s Simulation) CreateEvent(c *gin.Context) { sugar.SuccessResponse(c, 200, newEvent) } + +// Report fetches a simulation's report. +func (s Simulation) Report(c *gin.Context) { + user := middleware.GetUser(c) + var id string + if !bindID(c, &id) { + sugar.ErrResponse(c, 404, nil) + return + } + sim, err := s.Repo.ByIDForUser(id, user.ID) + if err != nil { + sugar.NotFoundOrError(c, err) + return + } + + report, err := s.Repo.GetReport(sim.ID) + if err != nil { + sugar.NotFoundOrError(c, err) + return + } + + sugar.SuccessResponse(c, 200, report) +} + +// CreateReport creates simulation report. +func (s Simulation) CreateReport(c *gin.Context) { + var id string + if !bindID(c, &id) { + sugar.ErrResponse(c, 404, nil) + return + } + sim, err := s.Repo.ByID(id) + if err != nil { + sugar.NotFoundOrError(c, err) + return + } + + if !isTokenAuthorized(c, sim.Token) { + c.AbortWithStatus(403) + return + } + + if c.ContentType() != "application/vnd.reconfigure.io/reports-v1+json" { + err = errors.New("Not a valid report version") + sugar.ErrResponse(c, 400, err) + return + } + + var report models.Report + err = c.BindJSON(&report) + if err != nil { + sugar.ErrResponse(c, 500, err) + return + } + + err = s.Repo.StoreReport(sim.ID, report) + if err != nil { + sugar.ErrResponse(c, 500, err) + return + } + + sugar.SuccessResponse(c, 200, nil) +} diff --git a/handlers/api/simulation_test.go b/handlers/api/simulation_test.go index edc8fc13..03f6304e 100644 --- a/handlers/api/simulation_test.go +++ b/handlers/api/simulation_test.go @@ -1,13 +1,20 @@ package api import ( + "net/http/httptest" + "strings" "testing" + "github.com/gin-gonic/gin" + + "github.com/ReconfigureIO/platform/models" "github.com/ReconfigureIO/platform/service/batch" "github.com/golang/mock/gomock" ) -func Test_ServiceInterface(t *testing.T) { +var emptyReport = "{\"moduleName\":\"\",\"partName\":\"\",\"lutSummary\":{\"description\":\"\",\"used\":0,\"available\":0,\"utilisation\":0,\"detail\":null},\"regSummary\":{\"description\":\"\",\"used\":0,\"available\":0,\"utilisation\":0,\"detail\":null},\"blockRamSummary\":{\"description\":\"\",\"used\":0,\"available\":0,\"utilisation\":0,\"detail\":null},\"ultraRamSummary\":{\"description\":\"\",\"used\":0,\"available\":0,\"utilisation\":0},\"dspBlockSummary\":{\"description\":\"\",\"used\":0,\"available\":0,\"utilisation\":0},\"weightedAverage\":{\"description\":\"\",\"used\":0,\"available\":0,\"utilisation\":0}}" + +func TestServiceInterface(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -18,3 +25,43 @@ func Test_ServiceInterface(t *testing.T) { t.Error("unexpected result") } } + +func TestSimulationReport(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + simRepo := models.NewMockSimulationRepo(mockCtrl) + simRepo.EXPECT().ByIDForUser("foosim", "foouser").Return(models.Simulation{ID: "foosim"}, nil) + simRepo.EXPECT().GetReport("foosim").Return(models.SimulationReport{}, nil) + s := Simulation{ + Repo: simRepo, + } + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Set("reco_user", models.User{ID: "foouser"}) + c.Params = append(c.Params, gin.Param{Key: "id", Value: "foosim"}) + s.Report(c) + if c.Writer.Status() != 200 { + t.Error("Expected 200 status, got: ", c.Writer.Status()) + } +} + +func TestSimulationCreateReport(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + simRepo := models.NewMockSimulationRepo(mockCtrl) + simRepo.EXPECT().ByID("foosim").Return(models.Simulation{ID: "foosim", Token: "footoken"}, nil) + simRepo.EXPECT().StoreReport("foosim", models.Report{}).Return(nil) + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest("GET", "/?token=footoken", strings.NewReader(emptyReport)) + c.Request.Header.Add("Content-Type", "application/vnd.reconfigure.io/reports-v1+json") + c.Params = append(c.Params, gin.Param{Key: "id", Value: "foosim"}) + + s := Simulation{ + Repo: simRepo, + } + s.CreateReport(c) + if c.Writer.Status() != 200 { + t.Error("Expected 200 status, got: ", c.Writer.Status()) + } +} diff --git a/main.go b/main.go index a57cc3cb..010fd1a3 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "github.com/ReconfigureIO/platform/config" "github.com/ReconfigureIO/platform/handlers/api" "github.com/ReconfigureIO/platform/migration" + "github.com/ReconfigureIO/platform/models" "github.com/ReconfigureIO/platform/routes" "github.com/ReconfigureIO/platform/service/auth" "github.com/ReconfigureIO/platform/service/auth/github" @@ -144,7 +145,7 @@ func main() { } // routes - routes.SetupRoutes(conf.Reco, conf.SecretKey, r, db, awsSession, events, leads, storageService, deploy, publicProjectID, authService) + routes.SetupRoutes(conf.Reco, conf.SecretKey, r, db, awsSession, events, leads, storageService, deploy, publicProjectID, authService, models.SimulationDataSource(db)) // queue var deploymentQueue queue.Queue diff --git a/routes/routes.go b/routes/routes.go index 88a39113..4be8b3b2 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -6,6 +6,7 @@ import ( "github.com/ReconfigureIO/platform/handlers/api" "github.com/ReconfigureIO/platform/handlers/profile" "github.com/ReconfigureIO/platform/middleware" + "github.com/ReconfigureIO/platform/models" "github.com/ReconfigureIO/platform/service/auth" "github.com/ReconfigureIO/platform/service/batch" "github.com/ReconfigureIO/platform/service/deployment" @@ -30,6 +31,7 @@ func SetupRoutes( deploy deployment.Service, publicProjectID string, authService auth.Service, + simRepo models.SimulationRepo, ) *gin.Engine { // setup common routes @@ -102,7 +104,7 @@ func SetupRoutes( projectRoute.GET("/:id", project.Get) } - simulation := api.NewSimulation(events, storage, awsService) + simulation := api.NewSimulation(events, storage, awsService, simRepo) simulationRoute := apiRoutes.Group("/simulations") { simulationRoute.GET("", simulation.List) @@ -110,6 +112,7 @@ func SetupRoutes( simulationRoute.GET("/:id", simulation.Get) simulationRoute.PUT("/:id/input", simulation.Input) simulationRoute.GET("/:id/logs", simulation.Logs) + simulationRoute.GET("/:id/reports", simulation.Report) } graph := api.Graph{ @@ -153,6 +156,7 @@ func SetupRoutes( reportRoutes := r.Group("", middleware.TokenAuth(db, events, config)) { reportRoutes.POST("/builds/:id/reports", build.CreateReport) + reportRoutes.POST("/simulations/:id/reports", simulation.CreateReport) } return r } diff --git a/routes/routes_test.go b/routes/routes_test.go index ee727f06..319c3e4f 100644 --- a/routes/routes_test.go +++ b/routes/routes_test.go @@ -43,7 +43,7 @@ func TestIndexHandler(t *testing.T) { // Setup router r := gin.Default() r.LoadHTMLGlob("../templates/*") - r = SetupRoutes(config.RecoConfig{}, "secretKey", r, db, nil, events, nil, nil, nil, "foobar", &auth.NOPService{}) + r = SetupRoutes(config.RecoConfig{}, "secretKey", r, db, nil, events, nil, nil, nil, "foobar", &auth.NOPService{}, nil) // Create a mock request to the index. req, err := http.NewRequest(http.MethodGet, "/", nil)