github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/jobs/jobs.go (about) 1 package jobs 2 3 import ( 4 "encoding/json" 5 "errors" 6 "io" 7 "net/http" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/justincampbell/bigduration" 13 14 "github.com/cozy/cozy-stack/model/bi" 15 "github.com/cozy/cozy-stack/model/instance" 16 "github.com/cozy/cozy-stack/model/job" 17 "github.com/cozy/cozy-stack/model/permission" 18 "github.com/cozy/cozy-stack/model/settings" 19 "github.com/cozy/cozy-stack/pkg/config/config" 20 "github.com/cozy/cozy-stack/pkg/consts" 21 "github.com/cozy/cozy-stack/pkg/couchdb" 22 "github.com/cozy/cozy-stack/pkg/emailer" 23 "github.com/cozy/cozy-stack/pkg/jsonapi" 24 "github.com/cozy/cozy-stack/pkg/limits" 25 "github.com/cozy/cozy-stack/pkg/mail" 26 "github.com/cozy/cozy-stack/pkg/metadata" 27 "github.com/cozy/cozy-stack/web/middlewares" 28 multierror "github.com/hashicorp/go-multierror" 29 "github.com/labstack/echo/v4" 30 31 // import workers 32 _ "github.com/cozy/cozy-stack/worker/archive" 33 "github.com/cozy/cozy-stack/worker/exec" 34 _ "github.com/cozy/cozy-stack/worker/log" 35 _ "github.com/cozy/cozy-stack/worker/mails" 36 _ "github.com/cozy/cozy-stack/worker/migrations" 37 _ "github.com/cozy/cozy-stack/worker/moves" 38 _ "github.com/cozy/cozy-stack/worker/notes" 39 _ "github.com/cozy/cozy-stack/worker/oauth" 40 _ "github.com/cozy/cozy-stack/worker/push" 41 _ "github.com/cozy/cozy-stack/worker/share" 42 _ "github.com/cozy/cozy-stack/worker/sms" 43 _ "github.com/cozy/cozy-stack/worker/thumbnail" 44 _ "github.com/cozy/cozy-stack/worker/trash" 45 ) 46 47 type ( 48 apiJob struct { 49 j *job.Job 50 } 51 apiJobRequest struct { 52 Arguments json.RawMessage `json:"arguments"` 53 Manual bool `json:"manual"` 54 ForwardLogs bool `json:"forward_logs"` 55 Options *apiJobOptions `json:"options"` 56 } 57 apiJobOptions struct { 58 MaxExecCount int `json:"max_exec_count"` 59 Timeout int `json:"timeout"` 60 } 61 apiSupport struct { 62 Arguments map[string]string `json:"arguments"` 63 } 64 apiCampaign struct { 65 Arguments apiCampaignArgs `json:"arguments"` 66 } 67 apiCampaignArgs struct { 68 Subject string `json:"subject"` 69 Parts []mail.Part `json:"parts"` 70 } 71 apiQueue struct { 72 workerType string 73 } 74 // apiTrigger is the jsonapi representation for a trigger 75 apiTrigger struct { 76 t *job.TriggerInfos 77 inst *instance.Instance 78 } 79 apiTriggerState struct { 80 t *job.TriggerInfos 81 s *job.TriggerState 82 } 83 apiTriggerRequest struct { 84 Type string `json:"type"` 85 Arguments string `json:"arguments"` 86 WorkerType string `json:"worker"` 87 Message json.RawMessage `json:"message"` 88 WorkerArguments json.RawMessage `json:"worker_arguments"` 89 Debounce string `json:"debounce"` 90 Options *job.JobOptions `json:"options"` 91 } 92 ) 93 94 func (j apiJob) ID() string { return j.j.ID() } 95 func (j apiJob) Rev() string { return j.j.Rev() } 96 func (j apiJob) DocType() string { return consts.Jobs } 97 func (j apiJob) Clone() couchdb.Doc { return j } 98 func (j apiJob) SetID(_ string) {} 99 func (j apiJob) SetRev(_ string) {} 100 func (j apiJob) Relationships() jsonapi.RelationshipMap { return nil } 101 func (j apiJob) Included() []jsonapi.Object { return nil } 102 func (j apiJob) Links() *jsonapi.LinksList { 103 return &jsonapi.LinksList{Self: "/jobs/" + j.j.WorkerType + "/" + j.j.ID()} 104 } 105 106 func (j apiJob) MarshalJSON() ([]byte, error) { 107 return json.Marshal(j.j) 108 } 109 110 func (q apiQueue) ID() string { return q.workerType } 111 func (q apiQueue) DocType() string { return consts.Jobs } 112 func (q apiQueue) Fetch(field string) []string { 113 switch field { 114 case "worker": 115 return []string{q.workerType} 116 default: 117 return nil 118 } 119 } 120 121 // NewAPITrigger creates a jsonapi representation of a trigger. 122 func NewAPITrigger(infos *job.TriggerInfos, inst *instance.Instance) jsonapi.Object { 123 return apiTrigger{infos, inst} 124 } 125 126 func (t apiTrigger) ID() string { return t.t.TID } 127 func (t apiTrigger) Rev() string { return "" } 128 func (t apiTrigger) DocType() string { return consts.Triggers } 129 func (t apiTrigger) Clone() couchdb.Doc { return t } 130 func (t apiTrigger) SetID(_ string) {} 131 func (t apiTrigger) SetRev(_ string) {} 132 func (t apiTrigger) Relationships() jsonapi.RelationshipMap { return nil } 133 func (t apiTrigger) Included() []jsonapi.Object { return nil } 134 func (t apiTrigger) Links() *jsonapi.LinksList { 135 links := &jsonapi.LinksList{Self: "/jobs/triggers/" + t.ID()} 136 if t.t.Type == "@webhook" { 137 links.Webhook = t.inst.PageURL("/jobs/webhooks/"+t.ID(), nil) 138 } 139 return links 140 } 141 142 func (t apiTrigger) MarshalJSON() ([]byte, error) { 143 return json.Marshal(t.t) 144 } 145 146 func (t apiTriggerState) ID() string { return t.t.TID } 147 func (t apiTriggerState) Rev() string { return "" } 148 func (t apiTriggerState) DocType() string { return consts.TriggersState } 149 func (t apiTriggerState) Clone() couchdb.Doc { return t } 150 func (t apiTriggerState) SetID(_ string) {} 151 func (t apiTriggerState) SetRev(_ string) {} 152 func (t apiTriggerState) Relationships() jsonapi.RelationshipMap { return nil } 153 func (t apiTriggerState) Included() []jsonapi.Object { return nil } 154 func (t apiTriggerState) Links() *jsonapi.LinksList { 155 return &jsonapi.LinksList{Self: "/jobs/triggers/" + t.ID() + "/state"} 156 } 157 158 func (t apiTriggerState) MarshalJSON() ([]byte, error) { 159 return json.Marshal(t.s) 160 } 161 162 const bearerAuthScheme = "Bearer " 163 164 // HTTPHandler handle all the `/jobs` routes. 165 type HTTPHandler struct { 166 emailer emailer.Emailer 167 } 168 169 // NewHTTPHandler instantiates a new [HTTPHandler]. 170 func NewHTTPHandler(emailer emailer.Emailer) *HTTPHandler { 171 return &HTTPHandler{emailer} 172 } 173 174 func (h *HTTPHandler) getQueue(c echo.Context) error { 175 instance := middlewares.GetInstance(c) 176 workerType := c.Param("worker-type") 177 178 o := apiQueue{workerType: workerType} 179 if err := middlewares.Allow(c, permission.GET, o); err != nil { 180 return err 181 } 182 183 js, err := job.GetQueuedJobs(instance, workerType) 184 if err != nil { 185 return wrapJobsError(err) 186 } 187 188 objs := make([]jsonapi.Object, len(js)) 189 for i, j := range js { 190 objs[i] = apiJob{j} 191 } 192 193 return jsonapi.DataList(c, http.StatusOK, objs, nil) 194 } 195 196 func (h *HTTPHandler) pushJob(c echo.Context) error { 197 instance := middlewares.GetInstance(c) 198 199 req := apiJobRequest{} 200 if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil { 201 return wrapJobsError(err) 202 } 203 var opts *job.JobOptions 204 if req.Options != nil { 205 opts = &job.JobOptions{ 206 MaxExecCount: req.Options.MaxExecCount, 207 Timeout: time.Duration(req.Options.Timeout) * time.Second, 208 } 209 } 210 211 jr := &job.JobRequest{ 212 WorkerType: c.Param("worker-type"), 213 Options: opts, 214 Manual: req.Manual, 215 ForwardLogs: req.ForwardLogs, 216 Message: job.Message(req.Arguments), 217 } 218 219 if err := middlewares.Allow(c, permission.POST, jr); err != nil { 220 return err 221 } 222 223 permd, err := middlewares.GetPermission(c) 224 if err != nil { 225 return err 226 } 227 if permd.Type != permission.TypeCLI { 228 if jr.ForwardLogs { 229 return echo.NewHTTPError(http.StatusForbidden) 230 } 231 if err := checkReservedWorker(jr.WorkerType); err != nil { 232 return err 233 } 234 } 235 236 j, err := job.System().PushJob(instance, jr) 237 if err != nil { 238 return wrapJobsError(err) 239 } 240 241 return jsonapi.Data(c, http.StatusAccepted, apiJob{j}, nil) 242 } 243 244 func (h *HTTPHandler) contactSupport(c echo.Context) error { 245 inst := middlewares.GetInstance(c) 246 247 req := apiSupport{} 248 if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil { 249 return wrapJobsError(err) 250 } 251 252 name, _ := settings.PublicName(inst) 253 msg, err := job.NewMessage(mail.Options{ 254 Mode: mail.ModeSupport, 255 TemplateName: "support_request", 256 TemplateValues: map[string]interface{}{ 257 "Name": name, 258 "Body": req.Arguments["body"], 259 }, 260 Subject: req.Arguments["subject"], 261 Layout: mail.CozyCloudLayout, 262 }) 263 if err != nil { 264 return err 265 } 266 jr := &job.JobRequest{WorkerType: "sendmail", Message: msg} 267 268 if err := middlewares.AllowWholeType(c, permission.POST, consts.Support); err != nil { 269 if middlewares.Allow(c, permission.POST, jr) != nil { 270 return err 271 } 272 } 273 274 if _, err = job.System().PushJob(inst, jr); err != nil { 275 return wrapJobsError(err) 276 } 277 return c.NoContent(http.StatusNoContent) 278 } 279 280 func (h *HTTPHandler) sendCampaignEmail(c echo.Context) error { 281 inst := middlewares.GetInstance(c) 282 283 if err := middlewares.Allow(c, permission.POST, &job.JobRequest{WorkerType: "sendmail"}); err != nil { 284 return err 285 } 286 287 req := apiCampaign{} 288 if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil { 289 return wrapJobsError(err) 290 } 291 292 err := h.emailer.SendCampaignEmail(inst, &emailer.CampaignEmailCmd{ 293 Subject: req.Arguments.Subject, 294 Parts: req.Arguments.Parts, 295 }) 296 if err != nil { 297 return wrapJobsError(err) 298 } 299 300 return c.NoContent(http.StatusNoContent) 301 } 302 303 func (h *HTTPHandler) newTrigger(c echo.Context) error { 304 instance := middlewares.GetInstance(c) 305 sched := job.System() 306 req := apiTriggerRequest{} 307 if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil { 308 return wrapJobsError(err) 309 } 310 311 if req.Debounce != "" { 312 if _, err := time.ParseDuration(req.Debounce); err != nil { 313 return jsonapi.InvalidAttribute("debounce", err) 314 } 315 } 316 317 // Handle metadata 318 md := metadata.New() 319 if claims := c.Get("claims"); claims != nil { 320 cl := claims.(permission.Claims) 321 if cl.Subject != "" { 322 md.CreatedByApp = cl.Subject 323 } 324 } 325 md.DocTypeVersion = job.DocTypeVersionTrigger 326 327 msg := req.Message 328 if req.Message == nil || len(req.Message) == 0 { 329 msg = req.WorkerArguments 330 } 331 t, err := job.NewTrigger(instance, job.TriggerInfos{ 332 Type: req.Type, 333 WorkerType: req.WorkerType, 334 Domain: instance.Domain, 335 Arguments: req.Arguments, 336 Debounce: req.Debounce, 337 Options: req.Options, 338 Metadata: md, 339 }, msg) 340 if err != nil { 341 return wrapJobsError(err) 342 } 343 if err = middlewares.Allow(c, permission.POST, t); err != nil { 344 return err 345 } 346 permd, err := middlewares.GetPermission(c) 347 if err != nil { 348 return err 349 } 350 if permd.Type != permission.TypeCLI { 351 if err := checkReservedWorker(req.WorkerType); err != nil { 352 return err 353 } 354 } 355 356 if err = sched.AddTrigger(t); err != nil { 357 return wrapJobsError(err) 358 } 359 return jsonapi.Data(c, http.StatusCreated, apiTrigger{t.Infos(), instance}, nil) 360 } 361 362 func (h *HTTPHandler) getTrigger(c echo.Context) error { 363 instance := middlewares.GetInstance(c) 364 sched := job.System() 365 t, err := sched.GetTrigger(instance, c.Param("trigger-id")) 366 if err != nil { 367 return wrapJobsError(err) 368 } 369 infos := t.Infos() 370 if err = middlewares.Allow(c, permission.GET, t); err != nil { 371 if !allowKonnectorForItsOwnTrigger(c, infos) { 372 return err 373 } 374 } 375 infos.CurrentState, err = job.GetTriggerState(t, t.ID()) 376 if err != nil { 377 return wrapJobsError(err) 378 } 379 return jsonapi.Data(c, http.StatusOK, apiTrigger{infos, instance}, nil) 380 } 381 382 func (h *HTTPHandler) getTriggerState(c echo.Context) error { 383 instance := middlewares.GetInstance(c) 384 sched := job.System() 385 t, err := sched.GetTrigger(instance, c.Param("trigger-id")) 386 if err != nil { 387 return wrapJobsError(err) 388 } 389 infos := t.Infos() 390 if err = middlewares.Allow(c, permission.GET, t); err != nil { 391 if !allowKonnectorForItsOwnTrigger(c, infos) { 392 return err 393 } 394 } 395 396 state, err := job.GetTriggerState(t, t.ID()) 397 if err != nil { 398 return wrapJobsError(err) 399 } 400 return jsonapi.Data(c, http.StatusOK, apiTriggerState{t: infos, s: state}, nil) 401 } 402 403 func (h *HTTPHandler) getTriggerJobs(c echo.Context) error { 404 instance := middlewares.GetInstance(c) 405 406 var err error 407 408 var limit int 409 if queryLimit := c.QueryParam("Limit"); queryLimit != "" { 410 limit, err = strconv.Atoi(queryLimit) 411 if err != nil { 412 return echo.NewHTTPError(http.StatusBadRequest, err) 413 } 414 } 415 416 sched := job.System() 417 t, err := sched.GetTrigger(instance, c.Param("trigger-id")) 418 if err != nil { 419 return wrapJobsError(err) 420 } 421 if err = middlewares.Allow(c, permission.GET, t); err != nil { 422 return err 423 } 424 425 js, err := job.GetJobs(t, t.ID(), limit) 426 if err != nil { 427 return wrapJobsError(err) 428 } 429 430 objs := make([]jsonapi.Object, len(js)) 431 for i, j := range js { 432 objs[i] = apiJob{j} 433 } 434 435 return jsonapi.DataList(c, http.StatusOK, objs, nil) 436 } 437 438 func (h *HTTPHandler) patchTrigger(c echo.Context) error { 439 inst := middlewares.GetInstance(c) 440 sched := job.System() 441 t, err := sched.GetTrigger(inst, c.Param("trigger-id")) 442 if err != nil { 443 return wrapJobsError(err) 444 } 445 infos := t.Infos() 446 if err := middlewares.Allow(c, permission.PATCH, t); err != nil { 447 if !allowKonnectorForItsOwnTrigger(c, infos) { 448 return err 449 } 450 } 451 452 req := apiTriggerRequest{} 453 if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil { 454 return wrapJobsError(err) 455 } 456 if req.Arguments == "" && len(req.Message) == 0 { 457 return jsonapi.BadRequest(errors.New("Only arguments and message can be patched")) 458 } 459 460 if len(req.Message) > 0 { 461 if err := sched.UpdateMessage(inst, t, req.Message); err != nil { 462 return wrapJobsError(err) 463 } 464 } 465 466 if req.Arguments != "" { 467 if err := sched.UpdateCron(inst, t, req.Arguments); err != nil { 468 return wrapJobsError(err) 469 } 470 } 471 472 return jsonapi.Data(c, http.StatusOK, apiTrigger{infos, inst}, nil) 473 } 474 475 func (h *HTTPHandler) launchTrigger(c echo.Context) error { 476 instance := middlewares.GetInstance(c) 477 t, err := job.System().GetTrigger(instance, c.Param("trigger-id")) 478 if err != nil { 479 return wrapJobsError(err) 480 } 481 if err = middlewares.Allow(c, permission.POST, t); err != nil { 482 return err 483 } 484 req := t.Infos().JobRequest() 485 req.Manual = true 486 j, err := job.System().PushJob(instance, req) 487 if err != nil { 488 return wrapJobsError(err) 489 } 490 if j.WorkerType == "client" { 491 if err := j.AckConsumed(); err != nil { 492 return wrapJobsError(err) 493 } 494 } 495 return jsonapi.Data(c, http.StatusCreated, apiJob{j}, nil) 496 } 497 498 func (h *HTTPHandler) deleteTrigger(c echo.Context) error { 499 instance := middlewares.GetInstance(c) 500 sched := job.System() 501 t, err := sched.GetTrigger(instance, c.Param("trigger-id")) 502 if err != nil { 503 return wrapJobsError(err) 504 } 505 infos := t.Infos() 506 if err := middlewares.Allow(c, permission.DELETE, t); err != nil { 507 if !allowKonnectorForItsOwnTrigger(c, infos) { 508 return err 509 } 510 } 511 if err := sched.DeleteTrigger(instance, c.Param("trigger-id")); err != nil { 512 return wrapJobsError(err) 513 } 514 return c.NoContent(http.StatusNoContent) 515 } 516 517 func (h *HTTPHandler) fireBIWebhook(c echo.Context) error { 518 inst := middlewares.GetInstance(c) 519 err := config.GetRateLimiter().CheckRateLimit(inst, limits.WebhookTriggerType) 520 if limits.IsLimitReachedOrExceeded(err) { 521 return echo.NewHTTPError(http.StatusNotFound, "Not found") 522 } 523 524 header := c.Request().Header.Get(echo.HeaderAuthorization) 525 if !strings.HasPrefix(header, bearerAuthScheme) { 526 return middlewares.ErrForbidden 527 } 528 token := strings.TrimPrefix(header, bearerAuthScheme) 529 530 var payload map[string]interface{} 531 if err := c.Bind(&payload); err != nil { 532 return jsonapi.BadRequest(err) 533 } 534 535 biEvent, err := bi.ParseEventBI(c.QueryParam("event")) 536 if err != nil { 537 return jsonapi.BadRequest(err) 538 } 539 540 // The stack will create or delete accounts and triggers on some webhooks, 541 // so it is safer to avoid concurrency on this part of the code. 542 mutex := config.Lock().ReadWrite(inst, "bi") 543 if err := mutex.Lock(); err != nil { 544 return err 545 } 546 defer mutex.Unlock() 547 548 call := &bi.WebhookCall{ 549 Instance: inst, 550 Token: token, 551 BIurl: c.QueryParam("bi_url"), 552 Event: biEvent, 553 Payload: payload, 554 } 555 if err := call.Fire(); err != nil { 556 return jsonapi.BadRequest(err) 557 } 558 return c.NoContent(http.StatusNoContent) 559 } 560 561 func (h *HTTPHandler) fireWebhook(c echo.Context) error { 562 inst := middlewares.GetInstance(c) 563 err := config.GetRateLimiter().CheckRateLimit(inst, limits.WebhookTriggerType) 564 if limits.IsLimitReachedOrExceeded(err) { 565 return echo.NewHTTPError(http.StatusNotFound, "Not found") 566 } 567 568 t, err := job.System().GetTrigger(inst, c.Param("trigger-id")) 569 if err != nil { 570 return wrapJobsError(err) 571 } 572 webhook, ok := t.(*job.WebhookTrigger) 573 if !ok { 574 return jsonapi.InvalidAttribute("Type", errors.New("Not a webhook")) 575 } 576 577 payload, err := io.ReadAll(c.Request().Body) 578 if err != nil { 579 return wrapJobsError(err) 580 } 581 582 manual := false 583 if c.QueryParam("Manual") == "true" { 584 manual = true 585 } 586 webhook.Fire(payload, manual) 587 return c.NoContent(http.StatusNoContent) 588 } 589 590 func (h *HTTPHandler) getAllTriggers(c echo.Context) error { 591 instance := middlewares.GetInstance(c) 592 593 var workerTypes, triggerTypes []string 594 if str := c.QueryParam("Worker"); str != "" { 595 workerTypes = strings.Split(str, ",") 596 } 597 if str := c.QueryParam("Type"); str != "" { 598 triggerTypes = strings.Split(str, ",") 599 } 600 601 if err := middlewares.AllowWholeType(c, permission.GET, consts.Triggers); err != nil { 602 if len(workerTypes) != 1 { 603 return err 604 } 605 o := &job.TriggerInfos{WorkerType: workerTypes[0]} 606 if err := middlewares.AllowOnFields(c, permission.GET, o, "worker"); err != nil { 607 return err 608 } 609 } 610 611 sched := job.System() 612 ts, err := sched.GetAllTriggers(instance) 613 if err != nil { 614 return wrapJobsError(err) 615 } 616 617 objs := make([]jsonapi.Object, 0, len(ts)) 618 for _, t := range ts { 619 tInfos := t.Infos() 620 if hasWorker(tInfos, workerTypes) && hasType(tInfos, triggerTypes) { 621 tInfos.CurrentState, err = job.GetTriggerState(t, t.ID()) 622 if err != nil { 623 return wrapJobsError(err) 624 } 625 objs = append(objs, apiTrigger{tInfos, instance}) 626 } 627 } 628 629 return jsonapi.DataList(c, http.StatusOK, objs, nil) 630 } 631 632 func hasWorker(infos *job.TriggerInfos, workers []string) bool { 633 if len(workers) == 0 { 634 return true 635 } 636 for _, w := range workers { 637 if infos.WorkerType == w { 638 return true 639 } 640 } 641 return false 642 } 643 644 func hasType(infos *job.TriggerInfos, triggerTypes []string) bool { 645 if len(triggerTypes) == 0 { 646 return true 647 } 648 for _, typ := range triggerTypes { 649 if infos.Type == typ { 650 return true 651 } 652 } 653 return false 654 } 655 656 func (h *HTTPHandler) getJob(c echo.Context) error { 657 instance := middlewares.GetInstance(c) 658 j, err := job.Get(instance, c.Param("job-id")) 659 if err != nil { 660 return err 661 } 662 if err := middlewares.Allow(c, permission.GET, j); err != nil { 663 return err 664 } 665 return jsonapi.Data(c, http.StatusOK, apiJob{j}, nil) 666 } 667 668 func (h *HTTPHandler) patchJob(c echo.Context) error { 669 inst := middlewares.GetInstance(c) 670 j, err := job.Get(inst, c.Param("job-id")) 671 if err != nil { 672 return err 673 } 674 if err := middlewares.Allow(c, permission.PATCH, j); err != nil { 675 return err 676 } 677 if j.WorkerType != "client" { 678 return middlewares.ErrForbidden 679 } 680 681 req := job.Job{} 682 if _, err := jsonapi.Bind(c.Request().Body, &req); err != nil { 683 return wrapJobsError(err) 684 } 685 686 log := inst.Logger(). 687 WithNamespace("jobs"). 688 WithField("job_id", j.ID()). 689 WithField("worker_id", "client") 690 691 msg := &exec.KonnectorMessage{} 692 693 if err := j.Message.Unmarshal(&msg); err == nil { 694 log = log. 695 WithField("slug", msg.Konnector). 696 WithField("account_id", msg.Account). 697 WithField("exec_time", time.Since(j.StartedAt)) 698 } 699 700 switch req.State { 701 case job.Errored: 702 err = j.Nack(req.Error) 703 log.Infof("Konnector failure: %s", req.Error) 704 log.Errorf("error while performing job: %s", req.Error) 705 case job.Done: 706 err = j.Ack() 707 log.Info("Konnector success") 708 default: 709 err = jsonapi.InvalidAttribute("State", errors.New("State must be done or errored")) 710 } 711 if err != nil { 712 return wrapJobsError(err) 713 } 714 715 return jsonapi.Data(c, http.StatusOK, apiJob{j}, nil) 716 } 717 718 func (h *HTTPHandler) cleanJobs(c echo.Context) error { 719 instance := middlewares.GetInstance(c) 720 if err := middlewares.AllowWholeType(c, permission.POST, consts.Jobs); err != nil { 721 return err 722 } 723 var ups []*job.Job 724 now := time.Now() 725 err := couchdb.ForeachDocs(instance, consts.Jobs, func(_ string, data json.RawMessage) error { 726 var j *job.Job 727 if err := json.Unmarshal(data, &j); err != nil { 728 return err 729 } 730 if j.State == job.Running || j.State == job.Queued { 731 if j.StartedAt.Add(1 * time.Hour).Before(now) { 732 ups = append(ups, j) 733 } 734 } 735 return nil 736 }) 737 if err != nil && !couchdb.IsNoDatabaseError(err) { 738 return err 739 } 740 var errf error 741 for _, j := range ups { 742 j.State = job.Done 743 err := couchdb.UpdateDoc(instance, j) 744 if err != nil { 745 errf = multierror.Append(errf, err) 746 } 747 } 748 if errf != nil { 749 return errf 750 } 751 return c.JSON(200, map[string]int{"deleted": len(ups)}) 752 } 753 754 func (h *HTTPHandler) purgeJobs(c echo.Context) error { 755 instance := middlewares.GetInstance(c) 756 if err := middlewares.AllowWholeType(c, permission.DELETE, consts.Jobs); err != nil { 757 return err 758 } 759 760 workersParam := c.QueryParam("workers") 761 durationParam := c.QueryParam("duration") 762 763 conf := config.GetConfig().Jobs 764 dur, err := bigduration.ParseDuration(conf.DefaultDurationToKeep) 765 if err != nil { 766 return err 767 } 768 769 if durationParam != "" { 770 dur, err = bigduration.ParseDuration(durationParam) 771 if err != nil { 772 return err 773 } 774 } 775 workers := job.GetWorkersNamesList() 776 if workersParam != "" { 777 workers = strings.Split(workersParam, ",") 778 } 779 780 allJobs, err := job.GetAllJobs(instance) 781 if err != nil { 782 return err 783 } 784 785 // Step 1: We want to get all the jobs prior to the date parameter. 786 // Jobs returned are the ones we want to remove 787 d := time.Now().Add(-dur) 788 jobsBeforeDate := job.FilterJobsBeforeDate(allJobs, d) 789 790 // Step 2: We also want to keep a minimum number of jobs for each state. 791 // Jobs returned will be kept. 792 lastsJobs := map[string]struct{}{} 793 for _, w := range workers { 794 jobs, err := job.GetLastsJobs(allJobs, w) 795 if err != nil { 796 return err 797 } 798 for _, j := range jobs { 799 lastsJobs[j.ID()] = struct{}{} 800 } 801 } 802 803 // Step 3: cleaning. 804 // - Removing jobs from the ids if they exists. 805 // - Skipping worker types 806 var finalJobs []*job.Job 807 808 for _, j := range jobsBeforeDate { 809 validWorker := false 810 811 for _, wt := range workers { 812 if j.WorkerType == wt { 813 validWorker = true 814 break 815 } 816 } 817 // Check the job is not existing in the lasts jobs 818 if validWorker { 819 _, ok := lastsJobs[j.ID()] 820 821 if !ok { 822 finalJobs = append(finalJobs, j) 823 } 824 } 825 } 826 827 // Bulk-deleting the jobs 828 jobsToDelete := make([]couchdb.Doc, len(finalJobs)) 829 for i, j := range finalJobs { 830 jobsToDelete[i] = j 831 } 832 833 chunkSize := 1000 834 835 for i := 0; i < len(jobsToDelete); i += chunkSize { 836 end := i + chunkSize 837 838 if end > len(jobsToDelete) { 839 end = len(jobsToDelete) 840 } 841 842 err = couchdb.BulkDeleteDocs(instance, consts.Jobs, jobsToDelete[i:end]) 843 if err != nil { 844 return err 845 } 846 } 847 848 return c.JSON(http.StatusOK, map[string]int{"deleted": len(jobsToDelete)}) 849 } 850 851 // Register all the `/jobs` routes to the given router 852 func (h *HTTPHandler) Register(router *echo.Group) { 853 router.GET("/queue/:worker-type", h.getQueue) 854 router.POST("/queue/:worker-type", h.pushJob) 855 router.POST("/support", h.contactSupport) 856 router.POST("/campaign-emails", h.sendCampaignEmail) 857 858 router.POST("/triggers", h.newTrigger) 859 router.GET("/triggers", h.getAllTriggers) 860 router.GET("/triggers/:trigger-id", h.getTrigger) 861 router.GET("/triggers/:trigger-id/state", h.getTriggerState) 862 router.GET("/triggers/:trigger-id/jobs", h.getTriggerJobs) 863 router.PATCH("/triggers/:trigger-id", h.patchTrigger) 864 router.POST("/triggers/:trigger-id/launch", h.launchTrigger) 865 router.DELETE("/triggers/:trigger-id", h.deleteTrigger) 866 867 router.POST("/webhooks/bi", h.fireBIWebhook) 868 router.POST("/webhooks/:trigger-id", h.fireWebhook) 869 870 router.POST("/clean", h.cleanJobs) 871 router.DELETE("/purge", h.purgeJobs) 872 router.GET("/:job-id", h.getJob) 873 router.PATCH("/:job-id", h.patchJob) 874 } 875 876 func wrapJobsError(err error) error { 877 switch err { 878 case job.ErrNotFoundTrigger, 879 job.ErrNotFoundJob, 880 job.ErrUnknownWorker: 881 return jsonapi.NotFound(err) 882 case job.ErrUnknownTrigger, 883 job.ErrNotCronTrigger: 884 return jsonapi.InvalidAttribute("Type", err) 885 case emailer.ErrMissingSubject, 886 emailer.ErrMissingContent, 887 limits.ErrRateLimitReached, 888 limits.ErrRateLimitExceeded: 889 return jsonapi.BadRequest(err) 890 } 891 return err 892 } 893 894 // checkReservedWorker returns an error if the worker should only by used by 895 // the stack, and the clients must not push jobs for it. 896 func checkReservedWorker(worker string) error { 897 reserved, err := job.System().WorkerIsReserved(worker) 898 if err != nil { 899 if errors.Is(err, job.ErrUnknownWorker) { 900 return echo.NewHTTPError(http.StatusNotFound) 901 } 902 return err 903 } 904 if reserved { 905 return echo.NewHTTPError(http.StatusForbidden) 906 } 907 return nil 908 } 909 910 func allowKonnectorForItsOwnTrigger(c echo.Context, infos *job.TriggerInfos) bool { 911 if infos.WorkerType != "konnector" { 912 return false 913 } 914 var msg map[string]interface{} 915 if errb := json.Unmarshal(infos.Message, &msg); errb != nil { 916 return false 917 } 918 slug, _ := msg["konnector"].(string) 919 if slug == "" { 920 return false 921 } 922 err := middlewares.AllowForKonnector(c, slug) 923 return err == nil 924 }