diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..58d9fa77 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,8 @@ +Platform v0.1.0 + +# Platform v0.1.0 + +## Features + - Added support for storing and retrieving Simulation Reports + +## Bugfixes \ No newline at end of file diff --git a/handlers/api/build.go b/handlers/api/build.go index 45592d59..6171ad20 100644 --- a/handlers/api/build.go +++ b/handlers/api/build.go @@ -295,7 +295,7 @@ func (b Build) CreateReport(c *gin.Context) { switch c.ContentType() { case "application/vnd.reconfigure.io/reports-v1+json": - report := models.ReportV1{} + report := models.Report{} c.BindJSON(&report) err = buildRepo.StoreBuildReport(build, report) default: 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/migration/migrate.go b/migration/migrate.go index 545ef2c0..17722216 100644 --- a/migration/migrate.go +++ b/migration/migrate.go @@ -10,6 +10,7 @@ import ( "github.com/ReconfigureIO/platform/migration/migration201801260952" "github.com/ReconfigureIO/platform/migration/migration201802231224" "github.com/ReconfigureIO/platform/migration/migration201807191024" + "github.com/ReconfigureIO/platform/migration/migration201809061242" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" @@ -24,6 +25,7 @@ var migrations = []*gormigrate.Migration{ &migration201801260952.Migration, &migration201802231224.Migration, &migration201807191024.Migration, + &migration201809061242.Migration, } // MigrateSchema performs database migration. diff --git a/migration/migration201809061242/migrate.go b/migration/migration201809061242/migrate.go new file mode 100644 index 00000000..8a28900d --- /dev/null +++ b/migration/migration201809061242/migrate.go @@ -0,0 +1,20 @@ +package migration201809061242 + +import ( + "errors" + + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/postgres" + gormigrate "gopkg.in/gormigrate.v1" +) + +var Migration = gormigrate.Migration{ + ID: "201809061242", + Migrate: func(tx *gorm.DB) error { + err := tx.AutoMigrate(&SimulationReport{}).Error + return err + }, + Rollback: func(tx *gorm.DB) error { + return errors.New("Migration failed. Hit rollback conditions while adding Simulation Reports table to DB") + }, +} diff --git a/migration/migration201809061242/models.go b/migration/migration201809061242/models.go new file mode 100644 index 00000000..39d89482 --- /dev/null +++ b/migration/migration201809061242/models.go @@ -0,0 +1,173 @@ +package migration201809061242 + +import ( + "time" + + "github.com/jinzhu/gorm" + uuid "github.com/satori/go.uuid" +) + +// uuidHook hooks new uuid as primary key for models before creation. +type uuidHook struct{} + +func (u uuidHook) BeforeCreate(scope *gorm.Scope) error { + return scope.SetColumn("id", uuid.NewV4().String()) +} + +// InviteToken model. +type InviteToken struct { + Token string `gorm:"type:varchar(128);primary_key" json:"token"` + IntercomId string `gorm:"type:varchar(128)" json:"-"` + Timestamp time.Time `json:"created_at"` +} + +type User struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + GithubID int `gorm:"unique_index" json:"-"` + GithubName string `json:"github_name"` + Name string `json:"name"` + Email string `gorm:"type:varchar(100);unique_index" json:"email"` + CreatedAt time.Time `json:"created_at"` + PhoneNumber string `json:"phone_number"` + Company string `json:"company"` + Landing string `json:"-"` + MainGoal string `json:"-"` + Employees string `json:"-"` + MarketVerticals string `json:"-"` + JobTitle string `json:"-"` + GithubAccessToken string `json:"-"` + Token string `json:"-"` + StripeToken string `json:"-"` + // We'll ignore this in the db for now, to provide mock data + BillingPlan string `gorm:"-" json:"billing_plan"` +} + +// Project model. +type Project struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + User User `json:"-" gorm:"ForeignKey:UserID"` // Project belongs to User + UserID string `json:"-"` + Name string `json:"name"` + Builds []Build `json:"builds,omitempty" gorm:"ForeignKey:ProjectID"` + Simulations []Build `json:"simulations,omitempty" gorm:"ForeignKey:ProjectID"` +} + +// Simulation model. +type Simulation struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + User User `json:"-" gorm:"ForeignKey:UserID"` + UserID int `json:"-"` + Project Project `json:"project,omitempty" gorm:"ForeignKey:ProjectID"` + ProjectID string `json:"-"` + BatchJobID int64 `json:"-"` + BatchJob BatchJob `json:"job" gorm:"ForeignKey:BatchJobId"` + Token string `json:"-"` + Command string `json:"command"` + Reports []Deployment `json:"deployments,omitempty" gorm:"ForeignKey:BuildID"` +} + +type SimulationReport struct { + uuidHook + ID string `gorm:"primary_key" json:"-"` + Simulation Simulation `json:"-" gorm:"ForeignKey:SimulationID"` + SimulationID string `json:"-"` + Version string `json:"-"` + Report string `json:"report" sql:"type:JSONB NOT NULL DEFAULT '{}'::JSONB"` +} + +// Deployment model. +type Deployment struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + Build Build `json:"build" gorm:"ForeignKey:BuildID"` + BuildID string `json:"-"` + Command string `json:"command"` + Token string `json:"-"` + InstanceID string `json:"-"` + UserID string `json:"-"` + IPAddress string `json:"ip_address"` + SpotInstance bool `json:"-" sql:"NOT NULL;DEFAULT:false"` + Events []DeploymentEvent `json:"events" gorm:"ForeignKey:DeploymentID"` +} + +// BatchJob model. +type BatchJob struct { + ID int64 `gorm:"primary_key" json:"-"` + BatchID string `json:"-"` + LogName string `json:"-"` + Events []BatchJobEvent `json:"events" gorm:"ForeignKey:BatchJobId"` +} + +// BatchJobEvent model. +type BatchJobEvent struct { + uuidHook + ID string `gorm:"primary_key" json:"-"` + BatchJobID int64 `json:"-"` + Timestamp time.Time `json:"timestamp"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + Code int `json:"code"` +} + +// DeploymentEvent model. +type DeploymentEvent struct { + uuidHook + ID string `gorm:"primary_key" json:"-"` + DeploymentID string `json:"-" validate:"nonzero"` + Timestamp time.Time `json:"timestamp"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + Code int `json:"code"` +} + +// Build model. +type Build struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + Project Project `json:"project" gorm:"ForeignKey:ProjectID"` + ProjectID string `json:"-"` + BatchJob BatchJob `json:"job" gorm:"ForeignKey:BatchJobId"` + BatchJobID int64 `json:"-"` + FPGAImage string `json:"-"` + Token string `json:"-"` + Message string `json:"message"` + Deployments []Deployment `json:"deployments,omitempty" gorm:"ForeignKey:BuildID"` +} + +type BuildReport struct { + uuidHook + ID string `gorm:"primary_key" json:"-"` + Build Build `json:"-" gorm:"ForeignKey:BuildID"` + BuildID string `json:"-"` + Version string `json:"-"` + Report string `json:"report" sql:"type:JSONB NOT NULL DEFAULT '{}'::JSONB"` +} + +// Graph model. +type Graph struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + Project Project `json:"project" gorm:"ForeignKey:ProjectID"` + ProjectID string `json:"-"` + BatchJob BatchJob `json:"job" gorm:"ForeignKey:BatchJobId"` + BatchJobID int64 `json:"-"` + Token string `json:"-"` + Type string `json:"type" gorm:"default:'dataflow'"` +} + +// QueueEntry is a queue entry. +type QueueEntry struct { + uuidHook + ID string `gorm:"primary_key"` + Type string `gorm:"default:'deployment'"` + TypeID string `gorm:"not_null"` + User User `json:"-" gorm:"ForeignKey:UserID"` + UserID string `json:"-"` + Weight int + Status string + CreatedAt time.Time + DispatchedAt time.Time +} diff --git a/models/build.go b/models/build.go index 4b170e02..4c9ba380 100644 --- a/models/build.go +++ b/models/build.go @@ -13,7 +13,7 @@ type BuildRepo interface { // Return a list of deployments, with the statuses specified, // limited to that number GetBuildsWithStatus([]string, int) ([]Build, error) - StoreBuildReport(Build, ReportV1) error + StoreBuildReport(Build, Report) error GetBuildReport(build Build) (BuildReport, error) } @@ -129,9 +129,9 @@ type BuildReport struct { Report string `json:"report" sql:"type:JSONB NOT NULL DEFAULT '{}'::JSONB"` } -// StoreBuildReport takes a build and reportV1, +// StoreBuildReport takes a build and Report, // and attaches the report to the build -func (repo *buildRepo) StoreBuildReport(build Build, report ReportV1) error { +func (repo *buildRepo) StoreBuildReport(build Build, report Report) error { db := repo.db newBytes, err := json.Marshal(&report) if err != nil { @@ -160,31 +160,3 @@ type PostBuild struct { ProjectID string `json:"project_id" validate:"nonzero"` Message string `json:"message"` } - -type ReportV1 struct { - ModuleName string `json:"moduleName"` - PartName string `json:"partName"` - LutSummary GroupSummary `json:"lutSummary"` - RegSummary GroupSummary `json:"regSummary"` - BlockRamSummary GroupSummary `json:"blockRamSummary"` - UltraRamSummary PartDetail `json:"ultraRamSummary"` - DspBlockSummary PartDetail `json:"dspBlockSummary"` - WeightedAverage PartDetail `json:"weightedAverage"` -} - -type GroupSummary struct { - Description string `json:"description"` - Used int `json:"used"` - Available int `json:"available"` - Utilisation float32 `json:"utilisation"` - Detail PartDetails `json:"detail"` -} - -type PartDetails map[string]PartDetail - -type PartDetail struct { - Description string `json:"description"` - Used int `json:"used"` - Available int `json:"available"` - Utilisation float32 `json:"utilisation"` -} diff --git a/models/build_serialise_test.go b/models/build_serialise_test.go index 48f7df2e..7a0539cd 100644 --- a/models/build_serialise_test.go +++ b/models/build_serialise_test.go @@ -7,7 +7,7 @@ import ( ) func TestSerialiseDeserialise(t *testing.T) { - report := &ReportV1{} + report := &Report{} reportContents := `{"moduleName":"fooModule","partName":"barPart","lutSummary":{"description":"CLB LUTs","used":70,"available":600577,"utilisation":0.01,"detail":{"lutLogic":{"description":"LUT as Logic","used":3,"available":600577,"utilisation":0.01},"lutMemory":{"description":"LUT as Memory","used":67,"available":394560,"utilisation":0.02}}},"regSummary":{"description":"CLB Registers","used":38,"available":1201154,"utilisation":0.01,"detail":{"regFlipFlop":{"description":"Register as Flip Flop","used":38,"available":1201154,"utilisation":0.01},"regLatch":{"description":"Register as Latch","used":0,"available":1201154,"utilisation":0}}},"blockRamSummary":{"description":"Block RAM Tile","used":0,"available":1024,"utilisation":0,"detail":{"blockRamB18":{"description":"RAMB18","used":0,"available":2048,"utilisation":0},"blockRamB36":{"description":"RAMB36/FIFO","used":0,"available":1024,"utilisation":0}}},"ultraRamSummary":{"description":"URAM","used":0,"available":470,"utilisation":0},"dspBlockSummary":{"description":"DSPs","used":0,"available":3474,"utilisation":0},"weightedAverage":{"description":"Weighted Average","used":318,"available":4569222,"utilisation":0.01}}` reportBytes := []byte(reportContents) err := json.Unmarshal(reportBytes, report) diff --git a/models/build_test.go b/models/build_test.go index 0a2749ae..b5eb2e44 100644 --- a/models/build_test.go +++ b/models/build_test.go @@ -49,7 +49,7 @@ func TestCreateBuildReport(t *testing.T) { // create a build in the DB build := Build{} db.Create(&build) - report := ReportV1{} + report := Report{} // run the get with status function err := d.StoreBuildReport(build, report) if err != nil { diff --git a/models/migrate.go b/models/migrate.go index 5922d587..82eebe30 100644 --- a/models/migrate.go +++ b/models/migrate.go @@ -17,4 +17,5 @@ func MigrateAll(db *gorm.DB) { db.AutoMigrate(&BuildReport{}) db.AutoMigrate(&Graph{}) db.AutoMigrate(&QueueEntry{}) + db.AutoMigrate(&SimulationReport{}) } diff --git a/models/models.go b/models/models.go index 596ec29c..dfd05f54 100644 --- a/models/models.go +++ b/models/models.go @@ -133,36 +133,6 @@ type PostDepEvent struct { Code int `json:"code"` } -// Simulation model. -type Simulation struct { - uuidHook - ID string `gorm:"primary_key" json:"id"` - User User `json:"-" gorm:"ForeignKey:UserID"` - UserID int `json:"-"` - Project Project `json:"project,omitempty" gorm:"ForeignKey:ProjectID"` - ProjectID string `json:"-"` - BatchJobID int64 `json:"-"` - BatchJob BatchJob `json:"job" gorm:"ForeignKey:BatchJobId"` - Token string `json:"-"` - Command string `json:"command"` -} - -// Status returns simulation status. -func (s *Simulation) Status() string { - events := s.BatchJob.Events - length := len(events) - if len(events) > 0 { - return events[length-1].Status - } - return StatusSubmitted -} - -// PostSimulation is the post request body for new simulation. -type PostSimulation struct { - ProjectID string `json:"project_id" validate:"nonzero"` - Command string `json:"command" validate:"nonzero"` -} - // Deployment model. type Deployment struct { uuidHook diff --git a/models/report.go b/models/report.go new file mode 100644 index 00000000..613d534a --- /dev/null +++ b/models/report.go @@ -0,0 +1,29 @@ +package models + +type Report struct { + ModuleName string `json:"moduleName"` + PartName string `json:"partName"` + LutSummary GroupSummary `json:"lutSummary"` + RegSummary GroupSummary `json:"regSummary"` + BlockRamSummary GroupSummary `json:"blockRamSummary"` + UltraRamSummary PartDetail `json:"ultraRamSummary"` + DspBlockSummary PartDetail `json:"dspBlockSummary"` + WeightedAverage PartDetail `json:"weightedAverage"` +} + +type GroupSummary struct { + Description string `json:"description"` + Used int `json:"used"` + Available int `json:"available"` + Utilisation float32 `json:"utilisation"` + Detail PartDetails `json:"detail"` +} + +type PartDetails map[string]PartDetail + +type PartDetail struct { + Description string `json:"description"` + Used int `json:"used"` + Available int `json:"available"` + Utilisation float32 `json:"utilisation"` +} diff --git a/models/simulation.go b/models/simulation.go new file mode 100644 index 00000000..2d1f95a5 --- /dev/null +++ b/models/simulation.go @@ -0,0 +1,106 @@ +package models + +//go:generate mockgen -source=simulation.go -package=models -destination=simulation_mock.go + +import ( + "encoding/json" + + "github.com/jinzhu/gorm" +) + +type SimulationRepo interface { + StoreReport(id string, report Report) error + GetReport(id string) (SimulationReport, error) + ByID(simulationID string) (Simulation, error) + ByIDForUser(simulationID, userID string) (Simulation, error) +} + +type simulationRepo struct{ db *gorm.DB } + +// SimulationDataSource returns the data source for simulations. +func SimulationDataSource(db *gorm.DB) SimulationRepo { + return &simulationRepo{db: db} +} + +// Simulation model. +type Simulation struct { + uuidHook + ID string `gorm:"primary_key" json:"id"` + User User `json:"-" gorm:"ForeignKey:UserID"` + UserID int `json:"-"` + Project Project `json:"project,omitempty" gorm:"ForeignKey:ProjectID"` + ProjectID string `json:"-"` + BatchJobID int64 `json:"-"` + BatchJob BatchJob `json:"job" gorm:"ForeignKey:BatchJobId"` + Token string `json:"-"` + Command string `json:"command"` +} + +// Status returns simulation status. +func (s *Simulation) Status() string { + events := s.BatchJob.Events + length := len(events) + if length > 0 { + return events[length-1].Status + } + return StatusSubmitted +} + +// PostSimulation is the post request body for new simulation. +type PostSimulation struct { + ProjectID string `json:"project_id" validate:"nonzero"` + Command string `json:"command" validate:"nonzero"` +} + +type SimulationReport struct { + uuidHook + ID string `gorm:"primary_key" json:"-"` + Simulation Simulation `json:"-" gorm:"ForeignKey:SimulationID"` + SimulationID string `json:"-"` + Version string `json:"-"` + Report string `json:"report" sql:"type:JSONB NOT NULL DEFAULT '{}'::JSONB"` +} + +// StoreReport takes a simulation and Report, and attaches the report to the +// simulation in our DB. +func (repo *simulationRepo) StoreReport(id string, report Report) error { + newBytes, err := json.Marshal(&report) + if err != nil { + return err + } + simReport := SimulationReport{ + SimulationID: id, + Version: "v1", + Report: string(newBytes), + } + err = repo.db.Create(&simReport).Error + return err +} + +// GetReport gets a simulation report given a simulation +func (repo *simulationRepo) GetReport(id string) (SimulationReport, error) { + var report SimulationReport + err := repo.db.Where("simulation_id = ?", id).First(&report).Error + return report, err +} + +func (repo *simulationRepo) preload() { + repo.db.Preload("Project"). + Preload("BatchJob"). + Preload("BatchJob.Events", func(db *gorm.DB) *gorm.DB { + return db.Order("timestamp ASC") + }) +} + +func (repo *simulationRepo) ByID(simID string) (Simulation, error) { + var sim Simulation + repo.preload() + err := repo.db.First(&sim, "simulations.id = ?", simID).Error + return sim, err +} + +func (repo *simulationRepo) ByIDForUser(simID string, userID string) (Simulation, error) { + repo.db.Joins("join projects on projects.id = simulations.project_id"). + Where("projects.user_id=?", userID) + return repo.ByID(simID) +} diff --git a/models/simulation_test.go b/models/simulation_test.go new file mode 100644 index 00000000..273c37b7 --- /dev/null +++ b/models/simulation_test.go @@ -0,0 +1,105 @@ +// +build integration + +package models + +import ( + "testing" + + "github.com/jinzhu/gorm" +) + +func TestStoreSimulationReport(t *testing.T) { + RunTransaction(func(db *gorm.DB) { + d := SimulationDataSource(db) + + var sim Simulation + db.Create(&sim) + + var report Report + err := d.StoreReport(sim.ID, report) + if err != nil { + t.Error(err) + return + } + return + }) +} + +func TestGetSimulationReport(t *testing.T) { + RunTransaction(func(db *gorm.DB) { + d := SimulationDataSource(db) + + report := SimulationReport{ + SimulationID: "foobar", + Report: "{}", + } + + db.Create(&report) + ret, err := d.GetReport(report.SimulationID) + if err != nil { + t.Error(err) + return + } + if ret.ID != report.ID { + t.Errorf("Expected: %v Got: %v", report, ret) + return + } + return + }) +} + +func TestSimulationByIDForUser(t *testing.T) { + RunTransaction(func(db *gorm.DB) { + users := []User{ + User{Email: "foo@bar.baz", GithubID: 1}, + User{Email: "baz@bar.foo", GithubID: 2}, + } + for _, user := range users { + db.Save(&user) + } + + projects := []Project{ + Project{UserID: users[0].ID}, + Project{UserID: users[1].ID}, + } + for _, project := range projects { + db.Save(&project) + } + + simulations := []Simulation{ + Simulation{ProjectID: projects[0].ID}, + Simulation{ProjectID: projects[0].ID}, + Simulation{ProjectID: projects[1].ID}, + Simulation{ProjectID: projects[1].ID}, + } + for _, simulation := range simulations { + db.Save(&simulation) + } + + var testCombos = []struct { + simID string + userID string + ret Simulation + }{ + {simulations[0].ID, users[0].ID, simulations[0]}, + {simulations[0].ID, users[1].ID, Simulation{}}, + {simulations[1].ID, users[0].ID, simulations[1]}, + {simulations[1].ID, users[1].ID, Simulation{}}, + {simulations[2].ID, users[0].ID, Simulation{}}, + {simulations[2].ID, users[1].ID, simulations[2]}, + {simulations[3].ID, users[0].ID, Simulation{}}, + {simulations[3].ID, users[1].ID, simulations[3]}, + } + d := SimulationDataSource(db) + for _, testCombo := range testCombos { + var sim Simulation + sim, err := d.ByIDForUser(testCombo.simID, testCombo.userID) + if sim.ID != testCombo.ret.ID { + t.Errorf("Error during testcombo: %v, %v", testCombo, err) + } + if sim.Project.UserID != testCombo.userID { + t.Error("Preload error during testcombo") + } + } + }) +} 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)