github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/apiv3/route/task_routes.go (about) 1 package route 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 9 "github.com/evergreen-ci/evergreen" 10 "github.com/evergreen-ci/evergreen/apiv3" 11 "github.com/evergreen-ci/evergreen/apiv3/model" 12 "github.com/evergreen-ci/evergreen/apiv3/servicecontext" 13 "github.com/evergreen-ci/evergreen/auth" 14 serviceModel "github.com/evergreen-ci/evergreen/model" 15 "github.com/evergreen-ci/evergreen/model/task" 16 "github.com/evergreen-ci/evergreen/util" 17 "github.com/gorilla/mux" 18 "github.com/pkg/errors" 19 ) 20 21 const ( 22 incorrectArgsTypeErrorMessage = "programmer error: incorrect type for paginator args" 23 ) 24 25 func getTaskRestartRouteManager(route string, version int) *RouteManager { 26 trh := &TaskRestartHandler{} 27 taskRestart := MethodHandler{ 28 PrefetchFunctions: []PrefetchFunc{PrefetchUser, PrefetchProjectContext}, 29 Authenticator: &RequireUserAuthenticator{}, 30 RequestHandler: trh.Handler(), 31 MethodType: evergreen.MethodPost, 32 } 33 34 taskRoute := RouteManager{ 35 Route: route, 36 Methods: []MethodHandler{taskRestart}, 37 Version: version, 38 } 39 return &taskRoute 40 } 41 42 func getTasksByBuildRouteManager(route string, version int) *RouteManager { 43 tbh := &tasksByBuildHandler{} 44 tasksByBuild := MethodHandler{ 45 PrefetchFunctions: []PrefetchFunc{PrefetchUser}, 46 Authenticator: &RequireUserAuthenticator{}, 47 RequestHandler: tbh.Handler(), 48 MethodType: evergreen.MethodGet, 49 } 50 51 taskRoute := RouteManager{ 52 Route: route, 53 Methods: []MethodHandler{tasksByBuild}, 54 Version: version, 55 } 56 return &taskRoute 57 } 58 59 func getTaskRouteManager(route string, version int) *RouteManager { 60 tep := &TaskExecutionPatchHandler{} 61 taskExecutionPatch := MethodHandler{ 62 PrefetchFunctions: []PrefetchFunc{PrefetchProjectContext, PrefetchUser}, 63 Authenticator: &NoAuthAuthenticator{}, 64 RequestHandler: tep.Handler(), 65 MethodType: evergreen.MethodPatch, 66 } 67 68 tgh := &taskGetHandler{} 69 taskGet := MethodHandler{ 70 PrefetchFunctions: []PrefetchFunc{PrefetchUser}, 71 Authenticator: &RequireUserAuthenticator{}, 72 RequestHandler: tgh.Handler(), 73 MethodType: evergreen.MethodGet, 74 } 75 76 taskRoute := RouteManager{ 77 Route: route, 78 Methods: []MethodHandler{taskExecutionPatch, taskGet}, 79 Version: version, 80 } 81 return &taskRoute 82 } 83 84 func getTasksByProjectAndCommitRouteManager(route string, version int) *RouteManager { 85 tph := &tasksByProjectHandler{} 86 tasksByProj := MethodHandler{ 87 PrefetchFunctions: []PrefetchFunc{PrefetchUser}, 88 Authenticator: &RequireUserAuthenticator{}, 89 RequestHandler: tph.Handler(), 90 MethodType: evergreen.MethodGet, 91 } 92 93 taskRoute := RouteManager{ 94 Route: route, 95 Methods: []MethodHandler{tasksByProj}, 96 Version: version, 97 } 98 return &taskRoute 99 } 100 101 // taskByProjectHandler implements the GET /projects/{project_id}/revisions/{commit_hash}/tasks. 102 // It fetches the associated tasks and returns them to the user. 103 type tasksByProjectHandler struct { 104 *PaginationExecutor 105 } 106 107 type tasksByProjectArgs struct { 108 projectId string 109 commitHash string 110 status string 111 } 112 113 // ParseAndValidate fetches the project context and task status from the request 114 // and loads them into the arguments to be used by the execution. 115 func (tph *tasksByProjectHandler) ParseAndValidate(r *http.Request) error { 116 args := tasksByProjectArgs{ 117 projectId: mux.Vars(r)["project_id"], 118 commitHash: mux.Vars(r)["commit_hash"], 119 status: r.URL.Query().Get("status"), 120 } 121 if args.projectId == "" { 122 return apiv3.APIError{ 123 Message: "ProjectId cannot be empty", 124 StatusCode: http.StatusBadRequest, 125 } 126 } 127 128 if args.commitHash == "" { 129 return apiv3.APIError{ 130 Message: "Revision cannot be empty", 131 StatusCode: http.StatusBadRequest, 132 } 133 } 134 tph.Args = args 135 return tph.PaginationExecutor.ParseAndValidate(r) 136 } 137 138 func tasksByProjectPaginator(key string, limit int, args interface{}, sc servicecontext.ServiceContext) ([]model.Model, 139 *PageResult, error) { 140 ptArgs, ok := args.(tasksByProjectArgs) 141 if !ok { 142 panic("ARGS HAD WRONG TYPE!") 143 } 144 tasks, err := sc.FindTasksByProjectAndCommit(ptArgs.projectId, ptArgs.commitHash, key, ptArgs.status, limit*2, 1) 145 if err != nil { 146 if _, ok := err.(*apiv3.APIError); !ok { 147 err = errors.Wrap(err, "Database error") 148 } 149 return []model.Model{}, nil, err 150 } 151 152 // Make the previous page 153 prevTasks, err := sc.FindTasksByProjectAndCommit(ptArgs.projectId, ptArgs.commitHash, key, ptArgs.status, limit, -1) 154 if err != nil { 155 if apiErr, ok := err.(*apiv3.APIError); !ok || apiErr.StatusCode != http.StatusNotFound { 156 return []model.Model{}, nil, errors.Wrap(err, "Database error") 157 } 158 } 159 160 nextPage := makeNextTasksPage(tasks, limit) 161 162 pageResults := &PageResult{ 163 Next: nextPage, 164 Prev: makePrevTasksPage(prevTasks), 165 } 166 167 lastIndex := len(tasks) 168 if nextPage != nil { 169 lastIndex = limit 170 } 171 172 // Truncate the tasks to just those that will be returned. 173 tasks = tasks[:lastIndex] 174 175 models := make([]model.Model, len(tasks)) 176 for ix, st := range tasks { 177 taskModel := &model.APITask{} 178 err = taskModel.BuildFromService(&st) 179 if err != nil { 180 return []model.Model{}, nil, err 181 } 182 err = taskModel.BuildFromService(sc.GetURL()) 183 if err != nil { 184 return []model.Model{}, nil, err 185 } 186 models[ix] = taskModel 187 } 188 return models, pageResults, nil 189 } 190 191 func makeNextTasksPage(tasks []task.Task, limit int) *Page { 192 var nextPage *Page 193 if len(tasks) > limit { 194 nextLimit := len(tasks) - limit 195 nextPage = &Page{ 196 Relation: "next", 197 Key: tasks[limit].Id, 198 Limit: nextLimit, 199 } 200 } 201 return nextPage 202 } 203 204 func makePrevTasksPage(tasks []task.Task) *Page { 205 var prevPage *Page 206 if len(tasks) > 1 { 207 prevPage = &Page{ 208 Relation: "prev", 209 Key: tasks[0].Id, 210 Limit: len(tasks), 211 } 212 } 213 return prevPage 214 } 215 216 func (tph *tasksByProjectHandler) Handler() RequestHandler { 217 taskPaginationExecutor := &PaginationExecutor{ 218 KeyQueryParam: "start_at", 219 LimitQueryParam: "limit", 220 Paginator: tasksByProjectPaginator, 221 222 Args: tasksByProjectArgs{}, 223 } 224 225 return &tasksByProjectHandler{taskPaginationExecutor} 226 } 227 228 // taskGetHandler implements the route GET /task/{task_id}. It fetches the associated 229 // task and returns it to the user. 230 type taskGetHandler struct { 231 taskId string 232 } 233 234 // ParseAndValidate fetches the taskId from the http request. 235 func (tgh *taskGetHandler) ParseAndValidate(r *http.Request) error { 236 vars := mux.Vars(r) 237 tgh.taskId = vars["task_id"] 238 return nil 239 } 240 241 // Execute calls the servicecontext FindTaskById function and returns the task 242 // from the provider. 243 func (tgh *taskGetHandler) Execute(sc servicecontext.ServiceContext) (ResponseData, error) { 244 foundTask, err := sc.FindTaskById(tgh.taskId) 245 if err != nil { 246 if _, ok := err.(*apiv3.APIError); !ok { 247 err = errors.Wrap(err, "Database error") 248 } 249 return ResponseData{}, err 250 } 251 252 taskModel := &model.APITask{} 253 err = taskModel.BuildFromService(foundTask) 254 if err != nil { 255 if _, ok := err.(*apiv3.APIError); !ok { 256 err = errors.Wrap(err, "API model error") 257 } 258 return ResponseData{}, err 259 } 260 261 err = taskModel.BuildFromService(sc.GetURL()) 262 if err != nil { 263 if _, ok := err.(*apiv3.APIError); !ok { 264 err = errors.Wrap(err, "API model error") 265 } 266 return ResponseData{}, err 267 } 268 269 return ResponseData{ 270 Result: []model.Model{taskModel}, 271 }, nil 272 } 273 274 func (trh *taskGetHandler) Handler() RequestHandler { 275 return &taskGetHandler{} 276 } 277 278 type tasksByBuildHandler struct { 279 *PaginationExecutor 280 } 281 282 type tasksByBuildArgs struct { 283 buildId string 284 status string 285 } 286 287 func (tbh *tasksByBuildHandler) ParseAndValidate(r *http.Request) error { 288 args := tasksByBuildArgs{ 289 buildId: mux.Vars(r)["build_id"], 290 status: r.URL.Query().Get("status"), 291 } 292 if args.buildId == "" { 293 return apiv3.APIError{ 294 Message: "buildId cannot be empty", 295 StatusCode: http.StatusBadRequest, 296 } 297 } 298 tbh.Args = args 299 return tbh.PaginationExecutor.ParseAndValidate(r) 300 } 301 302 func tasksByBuildPaginator(key string, limit int, args interface{}, sc servicecontext.ServiceContext) ([]model.Model, 303 *PageResult, error) { 304 btArgs, ok := args.(tasksByBuildArgs) 305 if !ok { 306 panic(incorrectArgsTypeErrorMessage) 307 } 308 // Fetch all of the tasks to be returned in this page plus the tasks used for 309 // calculating information about the next page. Here the limit is multiplied 310 // by two to fetch the next page. 311 tasks, err := sc.FindTasksByBuildId(btArgs.buildId, key, btArgs.status, limit*2, 1) 312 if err != nil { 313 if _, ok := err.(*apiv3.APIError); !ok { 314 err = errors.Wrap(err, "Database error") 315 } 316 return []model.Model{}, nil, err 317 } 318 319 // Fetch tasks to get information about the previous page. 320 prevTasks, err := sc.FindTasksByBuildId(btArgs.buildId, key, btArgs.status, limit, -1) 321 if err != nil { 322 if apiErr, ok := err.(*apiv3.APIError); !ok || apiErr.StatusCode != http.StatusNotFound { 323 return []model.Model{}, nil, errors.Wrap(err, "Database error") 324 } 325 } 326 327 nextPage := makeNextTasksPage(tasks, limit) 328 pageResults := &PageResult{ 329 Next: nextPage, 330 Prev: makePrevTasksPage(prevTasks), 331 } 332 333 lastIndex := len(tasks) 334 if nextPage != nil { 335 lastIndex = limit 336 } 337 338 // Truncate the tasks to just those that will be returned, removing the 339 // tasks that would be used to create the next page. 340 tasks = tasks[:lastIndex] 341 342 // Create an array of models which will be returned. 343 models := make([]model.Model, len(tasks)) 344 for ix, st := range tasks { 345 taskModel := &model.APITask{} 346 // Build an APIModel from the task and place it into the array. 347 err = taskModel.BuildFromService(&st) 348 if err != nil { 349 return []model.Model{}, nil, errors.Wrap(err, "API model error") 350 } 351 err = taskModel.BuildFromService(sc.GetURL()) 352 if err != nil { 353 return []model.Model{}, nil, errors.Wrap(err, "API model error") 354 } 355 models[ix] = taskModel 356 } 357 return models, pageResults, nil 358 } 359 360 func (hgh *tasksByBuildHandler) Handler() RequestHandler { 361 taskPaginationExecutor := &PaginationExecutor{ 362 KeyQueryParam: "start_at", 363 LimitQueryParam: "limit", 364 Paginator: tasksByBuildPaginator, 365 366 Args: tasksByBuildArgs{}, 367 } 368 369 return &tasksByBuildHandler{taskPaginationExecutor} 370 } 371 372 // TaskRestartHandler implements the route POST /task/{task_id}/restart. It 373 // fetches the needed task and project and calls the service function to 374 // set the proper fields when reseting the task. 375 type TaskRestartHandler struct { 376 taskId string 377 project *serviceModel.Project 378 379 username string 380 } 381 382 // ParseAndValidate fetches the taskId and Project from the request context and 383 // sets them on the TaskRestartHandler to be used by Execute. 384 func (trh *TaskRestartHandler) ParseAndValidate(r *http.Request) error { 385 projCtx := MustHaveProjectContext(r) 386 if projCtx.Task == nil { 387 return apiv3.APIError{ 388 Message: "Task not found", 389 StatusCode: http.StatusNotFound, 390 } 391 } 392 trh.taskId = projCtx.Task.Id 393 if projCtx.Project == nil { 394 return fmt.Errorf("Unable to fetch associated project") 395 } 396 trh.project = projCtx.Project 397 u := MustHaveUser(r) 398 trh.username = u.DisplayName() 399 return nil 400 } 401 402 // Execute calls the servicecontext ResetTask function and returns the refreshed 403 // task from the service. 404 func (trh *TaskRestartHandler) Execute(sc servicecontext.ServiceContext) (ResponseData, error) { 405 err := sc.ResetTask(trh.taskId, trh.username, trh.project) 406 if err != nil { 407 return ResponseData{}, 408 apiv3.APIError{ 409 Message: err.Error(), 410 StatusCode: http.StatusBadRequest, 411 } 412 } 413 414 refreshedTask, err := sc.FindTaskById(trh.taskId) 415 if err != nil { 416 return ResponseData{}, err 417 } 418 419 taskModel := &model.APITask{} 420 err = taskModel.BuildFromService(refreshedTask) 421 if err != nil { 422 if _, ok := err.(*apiv3.APIError); !ok { 423 err = errors.Wrap(err, "Database error") 424 } 425 return ResponseData{}, err 426 } 427 428 return ResponseData{ 429 Result: []model.Model{taskModel}, 430 }, nil 431 } 432 433 func (trh *TaskRestartHandler) Handler() RequestHandler { 434 return &TaskRestartHandler{} 435 } 436 437 // TaskExecutionPatchHandler implements the route PATCH /task/{task_id}. It 438 // fetches the changes from request, changes in activation and priority, and 439 // calls out to functions in the servicecontext to change these values. 440 type TaskExecutionPatchHandler struct { 441 Activated *bool `json:"activated"` 442 Priority *int64 `json:"priority"` 443 444 user auth.User 445 task *task.Task 446 } 447 448 // ParseAndValidate fetches the needed data from the request and errors otherwise. 449 // It fetches the task and user from the request context and fetches the changes 450 // in activation and priority from the request body. 451 func (tep *TaskExecutionPatchHandler) ParseAndValidate(r *http.Request) error { 452 body := util.NewRequestReader(r) 453 defer body.Close() 454 455 decoder := json.NewDecoder(body) 456 if err := decoder.Decode(tep); err != nil { 457 if err == io.EOF { 458 return apiv3.APIError{ 459 Message: "No request body sent", 460 StatusCode: http.StatusBadRequest, 461 } 462 } 463 if e, ok := err.(*json.UnmarshalTypeError); ok { 464 return apiv3.APIError{ 465 Message: fmt.Sprintf("Incorrect type given, expecting '%s' "+ 466 "but receieved '%s'", 467 e.Type, e.Value), 468 StatusCode: http.StatusBadRequest, 469 } 470 } 471 return errors.Wrap(err, "JSON unmarshal error") 472 } 473 474 if tep.Activated == nil && tep.Priority == nil { 475 return apiv3.APIError{ 476 Message: "Must set 'activated' or 'priority'", 477 StatusCode: http.StatusBadRequest, 478 } 479 } 480 projCtx := MustHaveProjectContext(r) 481 if projCtx.Task == nil { 482 return apiv3.APIError{ 483 Message: "Task not found", 484 StatusCode: http.StatusNotFound, 485 } 486 } 487 488 tep.task = projCtx.Task 489 u := MustHaveUser(r) 490 tep.user = u 491 return nil 492 } 493 494 // Execute sets the Activated and Priority field of the given task and returns 495 // an updated version of the task. 496 func (tep *TaskExecutionPatchHandler) Execute(sc servicecontext.ServiceContext) (ResponseData, error) { 497 if tep.Priority != nil { 498 priority := *tep.Priority 499 if priority > evergreen.MaxTaskPriority && 500 !auth.IsSuperUser(sc.GetSuperUsers(), tep.user) { 501 return ResponseData{}, apiv3.APIError{ 502 Message: fmt.Sprintf("Insufficient privilege to set priority to %d, "+ 503 "non-superusers can only set priority at or below %d", priority, evergreen.MaxTaskPriority), 504 StatusCode: http.StatusForbidden, 505 } 506 } 507 if err := sc.SetTaskPriority(tep.task, priority); err != nil { 508 return ResponseData{}, errors.Wrap(err, "Database error") 509 } 510 } 511 if tep.Activated != nil { 512 activated := *tep.Activated 513 if err := sc.SetTaskActivated(tep.task.Id, tep.user.Username(), activated); err != nil { 514 return ResponseData{}, errors.Wrap(err, "Database error") 515 } 516 } 517 refreshedTask, err := sc.FindTaskById(tep.task.Id) 518 if err != nil { 519 return ResponseData{}, errors.Wrap(err, "Database error") 520 } 521 522 taskModel := &model.APITask{} 523 err = taskModel.BuildFromService(refreshedTask) 524 if err != nil { 525 if _, ok := err.(*apiv3.APIError); !ok { 526 err = errors.Wrap(err, "Database error") 527 } 528 return ResponseData{}, err 529 } 530 531 return ResponseData{ 532 Result: []model.Model{taskModel}, 533 }, nil 534 } 535 536 func (tep *TaskExecutionPatchHandler) Handler() RequestHandler { 537 return &TaskExecutionPatchHandler{} 538 }