github.com/ncodes/nomad@v0.5.7-0.20170403112158-97adf4a74fb3/nomad/job_endpoint.go (about) 1 package nomad 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/armon/go-metrics" 10 "github.com/golang/snappy" 11 "github.com/hashicorp/consul/lib" 12 "github.com/hashicorp/go-memdb" 13 "github.com/hashicorp/go-multierror" 14 "github.com/ncodes/nomad/client/driver" 15 "github.com/ncodes/nomad/helper" 16 "github.com/ncodes/nomad/nomad/state" 17 "github.com/ncodes/nomad/nomad/structs" 18 "github.com/ncodes/nomad/scheduler" 19 ) 20 21 const ( 22 // RegisterEnforceIndexErrPrefix is the prefix to use in errors caused by 23 // enforcing the job modify index during registers. 24 RegisterEnforceIndexErrPrefix = "Enforcing job modify index" 25 26 // DispatchPayloadSizeLimit is the maximum size of the uncompressed input 27 // data payload. 28 DispatchPayloadSizeLimit = 16 * 1024 29 ) 30 31 var ( 32 // vaultConstraint is the implicit constraint added to jobs requesting a 33 // Vault token 34 vaultConstraint = &structs.Constraint{ 35 LTarget: "${attr.vault.version}", 36 RTarget: ">= 0.6.1", 37 Operand: structs.ConstraintVersion, 38 } 39 ) 40 41 // Job endpoint is used for job interactions 42 type Job struct { 43 srv *Server 44 } 45 46 // Register is used to upsert a job for scheduling 47 func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegisterResponse) error { 48 if done, err := j.srv.forward("Job.Register", args, args, reply); done { 49 return err 50 } 51 defer metrics.MeasureSince([]string{"nomad", "job", "register"}, time.Now()) 52 53 // Validate the arguments 54 if args.Job == nil { 55 return fmt.Errorf("missing job for registration") 56 } 57 58 // Initialize the job fields (sets defaults and any necessary init work). 59 args.Job.Canonicalize() 60 61 // Add implicit constraints 62 setImplicitConstraints(args.Job) 63 64 // Validate the job. 65 if err := validateJob(args.Job); err != nil { 66 return err 67 } 68 69 if args.EnforceIndex { 70 // Lookup the job 71 snap, err := j.srv.fsm.State().Snapshot() 72 if err != nil { 73 return err 74 } 75 ws := memdb.NewWatchSet() 76 job, err := snap.JobByID(ws, args.Job.ID) 77 if err != nil { 78 return err 79 } 80 jmi := args.JobModifyIndex 81 if job != nil { 82 if jmi == 0 { 83 return fmt.Errorf("%s 0: job already exists", RegisterEnforceIndexErrPrefix) 84 } else if jmi != job.JobModifyIndex { 85 return fmt.Errorf("%s %d: job exists with conflicting job modify index: %d", 86 RegisterEnforceIndexErrPrefix, jmi, job.JobModifyIndex) 87 } 88 } else if jmi != 0 { 89 return fmt.Errorf("%s %d: job does not exist", RegisterEnforceIndexErrPrefix, jmi) 90 } 91 } 92 93 // Ensure that the job has permissions for the requested Vault tokens 94 policies := args.Job.VaultPolicies() 95 if len(policies) != 0 { 96 vconf := j.srv.config.VaultConfig 97 if !vconf.IsEnabled() { 98 return fmt.Errorf("Vault not enabled and Vault policies requested") 99 } 100 101 // Have to check if the user has permissions 102 if !vconf.AllowsUnauthenticated() { 103 if args.Job.VaultToken == "" { 104 return fmt.Errorf("Vault policies requested but missing Vault Token") 105 } 106 107 vault := j.srv.vault 108 s, err := vault.LookupToken(context.Background(), args.Job.VaultToken) 109 if err != nil { 110 return err 111 } 112 113 allowedPolicies, err := PoliciesFrom(s) 114 if err != nil { 115 return err 116 } 117 118 // If we are given a root token it can access all policies 119 if !lib.StrContains(allowedPolicies, "root") { 120 flatPolicies := structs.VaultPoliciesSet(policies) 121 subset, offending := helper.SliceStringIsSubset(allowedPolicies, flatPolicies) 122 if !subset { 123 return fmt.Errorf("Passed Vault Token doesn't allow access to the following policies: %s", 124 strings.Join(offending, ", ")) 125 } 126 } 127 } 128 } 129 130 // Clear the Vault token 131 args.Job.VaultToken = "" 132 133 // Commit this update via Raft 134 _, index, err := j.srv.raftApply(structs.JobRegisterRequestType, args) 135 if err != nil { 136 j.srv.logger.Printf("[ERR] nomad.job: Register failed: %v", err) 137 return err 138 } 139 140 // Populate the reply with job information 141 reply.JobModifyIndex = index 142 143 // If the job is periodic or parameterized, we don't create an eval. 144 if args.Job.IsPeriodic() || args.Job.IsParameterized() { 145 return nil 146 } 147 148 // Create a new evaluation 149 eval := &structs.Evaluation{ 150 ID: structs.GenerateUUID(), 151 Priority: args.Job.Priority, 152 Type: args.Job.Type, 153 TriggeredBy: structs.EvalTriggerJobRegister, 154 JobID: args.Job.ID, 155 JobModifyIndex: index, 156 Status: structs.EvalStatusPending, 157 } 158 update := &structs.EvalUpdateRequest{ 159 Evals: []*structs.Evaluation{eval}, 160 WriteRequest: structs.WriteRequest{Region: args.Region}, 161 } 162 163 // Commit this evaluation via Raft 164 // XXX: There is a risk of partial failure where the JobRegister succeeds 165 // but that the EvalUpdate does not. 166 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 167 if err != nil { 168 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 169 return err 170 } 171 172 // Populate the reply with eval information 173 reply.EvalID = eval.ID 174 reply.EvalCreateIndex = evalIndex 175 reply.Index = evalIndex 176 return nil 177 } 178 179 // setImplicitConstraints adds implicit constraints to the job based on the 180 // features it is requesting. 181 func setImplicitConstraints(j *structs.Job) { 182 // Get the required Vault Policies 183 policies := j.VaultPolicies() 184 185 // Get the required signals 186 signals := j.RequiredSignals() 187 188 // Hot path 189 if len(signals) == 0 && len(policies) == 0 { 190 return 191 } 192 193 // Add Vault constraints 194 for _, tg := range j.TaskGroups { 195 _, ok := policies[tg.Name] 196 if !ok { 197 // Not requesting Vault 198 continue 199 } 200 201 found := false 202 for _, c := range tg.Constraints { 203 if c.Equal(vaultConstraint) { 204 found = true 205 break 206 } 207 } 208 209 if !found { 210 tg.Constraints = append(tg.Constraints, vaultConstraint) 211 } 212 } 213 214 // Add signal constraints 215 for _, tg := range j.TaskGroups { 216 tgSignals, ok := signals[tg.Name] 217 if !ok { 218 // Not requesting Vault 219 continue 220 } 221 222 // Flatten the signals 223 required := helper.MapStringStringSliceValueSet(tgSignals) 224 sigConstraint := getSignalConstraint(required) 225 226 found := false 227 for _, c := range tg.Constraints { 228 if c.Equal(sigConstraint) { 229 found = true 230 break 231 } 232 } 233 234 if !found { 235 tg.Constraints = append(tg.Constraints, sigConstraint) 236 } 237 } 238 } 239 240 // getSignalConstraint builds a suitable constraint based on the required 241 // signals 242 func getSignalConstraint(signals []string) *structs.Constraint { 243 return &structs.Constraint{ 244 Operand: structs.ConstraintSetContains, 245 LTarget: "${attr.os.signals}", 246 RTarget: strings.Join(signals, ","), 247 } 248 } 249 250 // Summary retreives the summary of a job 251 func (j *Job) Summary(args *structs.JobSummaryRequest, 252 reply *structs.JobSummaryResponse) error { 253 if done, err := j.srv.forward("Job.Summary", args, args, reply); done { 254 return err 255 } 256 defer metrics.MeasureSince([]string{"nomad", "job_summary", "get_job_summary"}, time.Now()) 257 // Setup the blocking query 258 opts := blockingOptions{ 259 queryOpts: &args.QueryOptions, 260 queryMeta: &reply.QueryMeta, 261 run: func(ws memdb.WatchSet, state *state.StateStore) error { 262 // Look for job summary 263 out, err := state.JobSummaryByID(ws, args.JobID) 264 if err != nil { 265 return err 266 } 267 268 // Setup the output 269 reply.JobSummary = out 270 if out != nil { 271 reply.Index = out.ModifyIndex 272 } else { 273 // Use the last index that affected the job_summary table 274 index, err := state.Index("job_summary") 275 if err != nil { 276 return err 277 } 278 reply.Index = index 279 } 280 281 // Set the query response 282 j.srv.setQueryMeta(&reply.QueryMeta) 283 return nil 284 }} 285 return j.srv.blockingRPC(&opts) 286 } 287 288 // Validate validates a job 289 func (j *Job) Validate(args *structs.JobValidateRequest, 290 reply *structs.JobValidateResponse) error { 291 292 if err := validateJob(args.Job); err != nil { 293 if merr, ok := err.(*multierror.Error); ok { 294 for _, err := range merr.Errors { 295 reply.ValidationErrors = append(reply.ValidationErrors, err.Error()) 296 } 297 reply.Error = merr.Error() 298 } else { 299 reply.ValidationErrors = append(reply.ValidationErrors, err.Error()) 300 reply.Error = err.Error() 301 } 302 } 303 reply.DriverConfigValidated = true 304 return nil 305 } 306 307 // Evaluate is used to force a job for re-evaluation 308 func (j *Job) Evaluate(args *structs.JobEvaluateRequest, reply *structs.JobRegisterResponse) error { 309 if done, err := j.srv.forward("Job.Evaluate", args, args, reply); done { 310 return err 311 } 312 defer metrics.MeasureSince([]string{"nomad", "job", "evaluate"}, time.Now()) 313 314 // Validate the arguments 315 if args.JobID == "" { 316 return fmt.Errorf("missing job ID for evaluation") 317 } 318 319 // Lookup the job 320 snap, err := j.srv.fsm.State().Snapshot() 321 if err != nil { 322 return err 323 } 324 ws := memdb.NewWatchSet() 325 job, err := snap.JobByID(ws, args.JobID) 326 if err != nil { 327 return err 328 } 329 if job == nil { 330 return fmt.Errorf("job not found") 331 } 332 333 if job.IsPeriodic() { 334 return fmt.Errorf("can't evaluate periodic job") 335 } else if job.IsParameterized() { 336 return fmt.Errorf("can't evaluate parameterized job") 337 } 338 339 // Create a new evaluation 340 eval := &structs.Evaluation{ 341 ID: structs.GenerateUUID(), 342 Priority: job.Priority, 343 Type: job.Type, 344 TriggeredBy: structs.EvalTriggerJobRegister, 345 JobID: job.ID, 346 JobModifyIndex: job.ModifyIndex, 347 Status: structs.EvalStatusPending, 348 } 349 update := &structs.EvalUpdateRequest{ 350 Evals: []*structs.Evaluation{eval}, 351 WriteRequest: structs.WriteRequest{Region: args.Region}, 352 } 353 354 // Commit this evaluation via Raft 355 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 356 if err != nil { 357 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 358 return err 359 } 360 361 // Setup the reply 362 reply.EvalID = eval.ID 363 reply.EvalCreateIndex = evalIndex 364 reply.JobModifyIndex = job.ModifyIndex 365 reply.Index = evalIndex 366 return nil 367 } 368 369 // Deregister is used to remove a job the cluster. 370 func (j *Job) Deregister(args *structs.JobDeregisterRequest, reply *structs.JobDeregisterResponse) error { 371 if done, err := j.srv.forward("Job.Deregister", args, args, reply); done { 372 return err 373 } 374 defer metrics.MeasureSince([]string{"nomad", "job", "deregister"}, time.Now()) 375 376 // Validate the arguments 377 if args.JobID == "" { 378 return fmt.Errorf("missing job ID for evaluation") 379 } 380 381 // Lookup the job 382 snap, err := j.srv.fsm.State().Snapshot() 383 if err != nil { 384 return err 385 } 386 ws := memdb.NewWatchSet() 387 job, err := snap.JobByID(ws, args.JobID) 388 if err != nil { 389 return err 390 } 391 392 // Commit this update via Raft 393 _, index, err := j.srv.raftApply(structs.JobDeregisterRequestType, args) 394 if err != nil { 395 j.srv.logger.Printf("[ERR] nomad.job: Deregister failed: %v", err) 396 return err 397 } 398 399 // Populate the reply with job information 400 reply.JobModifyIndex = index 401 402 // If the job is periodic or parameterized, we don't create an eval. 403 if job != nil && (job.IsPeriodic() || job.IsParameterized()) { 404 return nil 405 } 406 407 // Create a new evaluation 408 // XXX: The job priority / type is strange for this, since it's not a high 409 // priority even if the job was. The scheduler itself also doesn't matter, 410 // since all should be able to handle deregistration in the same way. 411 eval := &structs.Evaluation{ 412 ID: structs.GenerateUUID(), 413 Priority: structs.JobDefaultPriority, 414 Type: structs.JobTypeService, 415 TriggeredBy: structs.EvalTriggerJobDeregister, 416 JobID: args.JobID, 417 JobModifyIndex: index, 418 Status: structs.EvalStatusPending, 419 } 420 update := &structs.EvalUpdateRequest{ 421 Evals: []*structs.Evaluation{eval}, 422 WriteRequest: structs.WriteRequest{Region: args.Region}, 423 } 424 425 // Commit this evaluation via Raft 426 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 427 if err != nil { 428 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 429 return err 430 } 431 432 // Populate the reply with eval information 433 reply.EvalID = eval.ID 434 reply.EvalCreateIndex = evalIndex 435 reply.Index = evalIndex 436 return nil 437 } 438 439 // GetJob is used to request information about a specific job 440 func (j *Job) GetJob(args *structs.JobSpecificRequest, 441 reply *structs.SingleJobResponse) error { 442 if done, err := j.srv.forward("Job.GetJob", args, args, reply); done { 443 return err 444 } 445 defer metrics.MeasureSince([]string{"nomad", "job", "get_job"}, time.Now()) 446 447 // Setup the blocking query 448 opts := blockingOptions{ 449 queryOpts: &args.QueryOptions, 450 queryMeta: &reply.QueryMeta, 451 run: func(ws memdb.WatchSet, state *state.StateStore) error { 452 // Look for the job 453 out, err := state.JobByID(ws, args.JobID) 454 if err != nil { 455 return err 456 } 457 458 // Setup the output 459 reply.Job = out 460 if out != nil { 461 reply.Index = out.ModifyIndex 462 } else { 463 // Use the last index that affected the nodes table 464 index, err := state.Index("jobs") 465 if err != nil { 466 return err 467 } 468 reply.Index = index 469 } 470 471 // Set the query response 472 j.srv.setQueryMeta(&reply.QueryMeta) 473 return nil 474 }} 475 return j.srv.blockingRPC(&opts) 476 } 477 478 // List is used to list the jobs registered in the system 479 func (j *Job) List(args *structs.JobListRequest, 480 reply *structs.JobListResponse) error { 481 if done, err := j.srv.forward("Job.List", args, args, reply); done { 482 return err 483 } 484 defer metrics.MeasureSince([]string{"nomad", "job", "list"}, time.Now()) 485 486 // Setup the blocking query 487 opts := blockingOptions{ 488 queryOpts: &args.QueryOptions, 489 queryMeta: &reply.QueryMeta, 490 run: func(ws memdb.WatchSet, state *state.StateStore) error { 491 // Capture all the jobs 492 var err error 493 var iter memdb.ResultIterator 494 if prefix := args.QueryOptions.Prefix; prefix != "" { 495 iter, err = state.JobsByIDPrefix(ws, prefix) 496 } else { 497 iter, err = state.Jobs(ws) 498 } 499 if err != nil { 500 return err 501 } 502 503 var jobs []*structs.JobListStub 504 for { 505 raw := iter.Next() 506 if raw == nil { 507 break 508 } 509 job := raw.(*structs.Job) 510 summary, err := state.JobSummaryByID(ws, job.ID) 511 if err != nil { 512 return fmt.Errorf("unable to look up summary for job: %v", job.ID) 513 } 514 jobs = append(jobs, job.Stub(summary)) 515 } 516 reply.Jobs = jobs 517 518 // Use the last index that affected the jobs table 519 index, err := state.Index("jobs") 520 if err != nil { 521 return err 522 } 523 reply.Index = index 524 525 // Set the query response 526 j.srv.setQueryMeta(&reply.QueryMeta) 527 return nil 528 }} 529 return j.srv.blockingRPC(&opts) 530 } 531 532 // Allocations is used to list the allocations for a job 533 func (j *Job) Allocations(args *structs.JobSpecificRequest, 534 reply *structs.JobAllocationsResponse) error { 535 if done, err := j.srv.forward("Job.Allocations", args, args, reply); done { 536 return err 537 } 538 defer metrics.MeasureSince([]string{"nomad", "job", "allocations"}, time.Now()) 539 540 // Setup the blocking query 541 opts := blockingOptions{ 542 queryOpts: &args.QueryOptions, 543 queryMeta: &reply.QueryMeta, 544 run: func(ws memdb.WatchSet, state *state.StateStore) error { 545 // Capture the allocations 546 allocs, err := state.AllocsByJob(ws, args.JobID, args.AllAllocs) 547 if err != nil { 548 return err 549 } 550 551 // Convert to stubs 552 if len(allocs) > 0 { 553 reply.Allocations = make([]*structs.AllocListStub, 0, len(allocs)) 554 for _, alloc := range allocs { 555 reply.Allocations = append(reply.Allocations, alloc.Stub()) 556 } 557 } 558 559 // Use the last index that affected the allocs table 560 index, err := state.Index("allocs") 561 if err != nil { 562 return err 563 } 564 reply.Index = index 565 566 // Set the query response 567 j.srv.setQueryMeta(&reply.QueryMeta) 568 return nil 569 570 }} 571 return j.srv.blockingRPC(&opts) 572 } 573 574 // Evaluations is used to list the evaluations for a job 575 func (j *Job) Evaluations(args *structs.JobSpecificRequest, 576 reply *structs.JobEvaluationsResponse) error { 577 if done, err := j.srv.forward("Job.Evaluations", args, args, reply); done { 578 return err 579 } 580 defer metrics.MeasureSince([]string{"nomad", "job", "evaluations"}, time.Now()) 581 582 // Setup the blocking query 583 opts := blockingOptions{ 584 queryOpts: &args.QueryOptions, 585 queryMeta: &reply.QueryMeta, 586 run: func(ws memdb.WatchSet, state *state.StateStore) error { 587 // Capture the evals 588 var err error 589 reply.Evaluations, err = state.EvalsByJob(ws, args.JobID) 590 if err != nil { 591 return err 592 } 593 594 // Use the last index that affected the evals table 595 index, err := state.Index("evals") 596 if err != nil { 597 return err 598 } 599 reply.Index = index 600 601 // Set the query response 602 j.srv.setQueryMeta(&reply.QueryMeta) 603 return nil 604 }} 605 606 return j.srv.blockingRPC(&opts) 607 } 608 609 // Plan is used to cause a dry-run evaluation of the Job and return the results 610 // with a potential diff containing annotations. 611 func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse) error { 612 if done, err := j.srv.forward("Job.Plan", args, args, reply); done { 613 return err 614 } 615 defer metrics.MeasureSince([]string{"nomad", "job", "plan"}, time.Now()) 616 617 // Validate the arguments 618 if args.Job == nil { 619 return fmt.Errorf("Job required for plan") 620 } 621 622 // Initialize the job fields (sets defaults and any necessary init work). 623 args.Job.Canonicalize() 624 625 // Add implicit constraints 626 setImplicitConstraints(args.Job) 627 628 // Validate the job. 629 if err := validateJob(args.Job); err != nil { 630 return err 631 } 632 633 // Acquire a snapshot of the state 634 snap, err := j.srv.fsm.State().Snapshot() 635 if err != nil { 636 return err 637 } 638 639 // Get the original job 640 ws := memdb.NewWatchSet() 641 oldJob, err := snap.JobByID(ws, args.Job.ID) 642 if err != nil { 643 return err 644 } 645 646 var index uint64 647 var updatedIndex uint64 648 if oldJob != nil { 649 index = oldJob.JobModifyIndex 650 updatedIndex = oldJob.JobModifyIndex + 1 651 } 652 653 // Insert the updated Job into the snapshot 654 snap.UpsertJob(updatedIndex, args.Job) 655 656 // Create an eval and mark it as requiring annotations and insert that as well 657 eval := &structs.Evaluation{ 658 ID: structs.GenerateUUID(), 659 Priority: args.Job.Priority, 660 Type: args.Job.Type, 661 TriggeredBy: structs.EvalTriggerJobRegister, 662 JobID: args.Job.ID, 663 JobModifyIndex: updatedIndex, 664 Status: structs.EvalStatusPending, 665 AnnotatePlan: true, 666 } 667 668 // Create an in-memory Planner that returns no errors and stores the 669 // submitted plan and created evals. 670 planner := &scheduler.Harness{ 671 State: &snap.StateStore, 672 } 673 674 // Create the scheduler and run it 675 sched, err := scheduler.NewScheduler(eval.Type, j.srv.logger, snap, planner) 676 if err != nil { 677 return err 678 } 679 680 if err := sched.Process(eval); err != nil { 681 return err 682 } 683 684 // Annotate and store the diff 685 if plans := len(planner.Plans); plans != 1 { 686 return fmt.Errorf("scheduler resulted in an unexpected number of plans: %v", plans) 687 } 688 annotations := planner.Plans[0].Annotations 689 if args.Diff { 690 jobDiff, err := oldJob.Diff(args.Job, true) 691 if err != nil { 692 return fmt.Errorf("failed to create job diff: %v", err) 693 } 694 695 if err := scheduler.Annotate(jobDiff, annotations); err != nil { 696 return fmt.Errorf("failed to annotate job diff: %v", err) 697 } 698 reply.Diff = jobDiff 699 } 700 701 // Grab the failures 702 if len(planner.Evals) != 1 { 703 return fmt.Errorf("scheduler resulted in an unexpected number of eval updates: %v", planner.Evals) 704 } 705 updatedEval := planner.Evals[0] 706 707 // If it is a periodic job calculate the next launch 708 if args.Job.IsPeriodic() && args.Job.Periodic.Enabled { 709 reply.NextPeriodicLaunch = args.Job.Periodic.Next(time.Now().In(args.Job.Periodic.GetLocation())) 710 } 711 712 reply.FailedTGAllocs = updatedEval.FailedTGAllocs 713 reply.JobModifyIndex = index 714 reply.Annotations = annotations 715 reply.CreatedEvals = planner.CreateEvals 716 reply.Index = index 717 return nil 718 } 719 720 // validateJob validates a Job and task drivers and returns an error if there is 721 // a validation problem or if the Job is of a type a user is not allowed to 722 // submit. 723 func validateJob(job *structs.Job) error { 724 validationErrors := new(multierror.Error) 725 if err := job.Validate(); err != nil { 726 multierror.Append(validationErrors, err) 727 } 728 729 // Get the signals required 730 signals := job.RequiredSignals() 731 732 // Validate the driver configurations. 733 for _, tg := range job.TaskGroups { 734 // Get the signals for the task group 735 tgSignals, tgOk := signals[tg.Name] 736 737 for _, task := range tg.Tasks { 738 d, err := driver.NewDriver( 739 task.Driver, 740 driver.NewEmptyDriverContext(), 741 ) 742 if err != nil { 743 msg := "failed to create driver for task %q in group %q for validation: %v" 744 multierror.Append(validationErrors, fmt.Errorf(msg, tg.Name, task.Name, err)) 745 continue 746 } 747 748 if err := d.Validate(task.Config); err != nil { 749 formatted := fmt.Errorf("group %q -> task %q -> config: %v", tg.Name, task.Name, err) 750 multierror.Append(validationErrors, formatted) 751 } 752 753 // The task group didn't have any task that required signals 754 if !tgOk { 755 continue 756 } 757 758 // This task requires signals. Ensure the driver is capable 759 if required, ok := tgSignals[task.Name]; ok { 760 abilities := d.Abilities() 761 if !abilities.SendSignals { 762 formatted := fmt.Errorf("group %q -> task %q: driver %q doesn't support sending signals. Requested signals are %v", 763 tg.Name, task.Name, task.Driver, strings.Join(required, ", ")) 764 multierror.Append(validationErrors, formatted) 765 } 766 } 767 } 768 } 769 770 if job.Type == structs.JobTypeCore { 771 multierror.Append(validationErrors, fmt.Errorf("job type cannot be core")) 772 } 773 774 if len(job.Payload) != 0 { 775 multierror.Append(validationErrors, fmt.Errorf("job can't be submitted with a payload, only dispatched")) 776 } 777 778 return validationErrors.ErrorOrNil() 779 } 780 781 // Dispatch a parameterized job. 782 func (j *Job) Dispatch(args *structs.JobDispatchRequest, reply *structs.JobDispatchResponse) error { 783 if done, err := j.srv.forward("Job.Dispatch", args, args, reply); done { 784 return err 785 } 786 defer metrics.MeasureSince([]string{"nomad", "job", "dispatch"}, time.Now()) 787 788 // Lookup the parameterized job 789 if args.JobID == "" { 790 return fmt.Errorf("missing parameterized job ID") 791 } 792 793 snap, err := j.srv.fsm.State().Snapshot() 794 if err != nil { 795 return err 796 } 797 ws := memdb.NewWatchSet() 798 parameterizedJob, err := snap.JobByID(ws, args.JobID) 799 if err != nil { 800 return err 801 } 802 if parameterizedJob == nil { 803 return fmt.Errorf("parameterized job not found") 804 } 805 806 if !parameterizedJob.IsParameterized() { 807 return fmt.Errorf("Specified job %q is not a parameterized job", args.JobID) 808 } 809 810 // Validate the arguments 811 if err := validateDispatchRequest(args, parameterizedJob); err != nil { 812 return err 813 } 814 815 // Derive the child job and commit it via Raft 816 dispatchJob := parameterizedJob.Copy() 817 dispatchJob.ParameterizedJob = nil 818 dispatchJob.ID = structs.DispatchedID(parameterizedJob.ID, time.Now()) 819 dispatchJob.ParentID = parameterizedJob.ID 820 dispatchJob.Name = dispatchJob.ID 821 822 // Merge in the meta data 823 for k, v := range args.Meta { 824 if dispatchJob.Meta == nil { 825 dispatchJob.Meta = make(map[string]string, len(args.Meta)) 826 } 827 dispatchJob.Meta[k] = v 828 } 829 830 // Compress the payload 831 dispatchJob.Payload = snappy.Encode(nil, args.Payload) 832 833 regReq := &structs.JobRegisterRequest{ 834 Job: dispatchJob, 835 WriteRequest: args.WriteRequest, 836 } 837 838 // Commit this update via Raft 839 _, jobCreateIndex, err := j.srv.raftApply(structs.JobRegisterRequestType, regReq) 840 if err != nil { 841 j.srv.logger.Printf("[ERR] nomad.job: Dispatched job register failed: %v", err) 842 return err 843 } 844 845 reply.JobCreateIndex = jobCreateIndex 846 reply.DispatchedJobID = dispatchJob.ID 847 reply.Index = jobCreateIndex 848 849 // If the job is periodic, we don't create an eval. 850 if !dispatchJob.IsPeriodic() { 851 // Create a new evaluation 852 eval := &structs.Evaluation{ 853 ID: structs.GenerateUUID(), 854 Priority: dispatchJob.Priority, 855 Type: dispatchJob.Type, 856 TriggeredBy: structs.EvalTriggerJobRegister, 857 JobID: dispatchJob.ID, 858 JobModifyIndex: jobCreateIndex, 859 Status: structs.EvalStatusPending, 860 } 861 update := &structs.EvalUpdateRequest{ 862 Evals: []*structs.Evaluation{eval}, 863 WriteRequest: structs.WriteRequest{Region: args.Region}, 864 } 865 866 // Commit this evaluation via Raft 867 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 868 if err != nil { 869 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 870 return err 871 } 872 873 // Setup the reply 874 reply.EvalID = eval.ID 875 reply.EvalCreateIndex = evalIndex 876 reply.Index = evalIndex 877 } 878 879 return nil 880 } 881 882 // validateDispatchRequest returns whether the request is valid given the 883 // parameterized job. 884 func validateDispatchRequest(req *structs.JobDispatchRequest, job *structs.Job) error { 885 // Check the payload constraint is met 886 hasInputData := len(req.Payload) != 0 887 if job.ParameterizedJob.Payload == structs.DispatchPayloadRequired && !hasInputData { 888 return fmt.Errorf("Payload is not provided but required by parameterized job") 889 } else if job.ParameterizedJob.Payload == structs.DispatchPayloadForbidden && hasInputData { 890 return fmt.Errorf("Payload provided but forbidden by parameterized job") 891 } 892 893 // Check the payload doesn't exceed the size limit 894 if l := len(req.Payload); l > DispatchPayloadSizeLimit { 895 return fmt.Errorf("Payload exceeds maximum size; %d > %d", l, DispatchPayloadSizeLimit) 896 } 897 898 // Check if the metadata is a set 899 keys := make(map[string]struct{}, len(req.Meta)) 900 for k := range keys { 901 if _, ok := keys[k]; ok { 902 return fmt.Errorf("Duplicate key %q in passed metadata", k) 903 } 904 keys[k] = struct{}{} 905 } 906 907 required := helper.SliceStringToSet(job.ParameterizedJob.MetaRequired) 908 optional := helper.SliceStringToSet(job.ParameterizedJob.MetaOptional) 909 910 // Check the metadata key constraints are met 911 unpermitted := make(map[string]struct{}) 912 for k := range req.Meta { 913 _, req := required[k] 914 _, opt := optional[k] 915 if !req && !opt { 916 unpermitted[k] = struct{}{} 917 } 918 } 919 920 if len(unpermitted) != 0 { 921 flat := make([]string, 0, len(unpermitted)) 922 for k := range unpermitted { 923 flat = append(flat, k) 924 } 925 926 return fmt.Errorf("Dispatch request included unpermitted metadata keys: %v", flat) 927 } 928 929 missing := make(map[string]struct{}) 930 for _, k := range job.ParameterizedJob.MetaRequired { 931 if _, ok := req.Meta[k]; !ok { 932 missing[k] = struct{}{} 933 } 934 } 935 936 if len(missing) != 0 { 937 flat := make([]string, 0, len(missing)) 938 for k := range missing { 939 flat = append(flat, k) 940 } 941 942 return fmt.Errorf("Dispatch did not provide required meta keys: %v", flat) 943 } 944 945 return nil 946 }