github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/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/hashicorp/nomad/client/driver" 15 "github.com/hashicorp/nomad/helper" 16 "github.com/hashicorp/nomad/nomad/state" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/hashicorp/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, reply *structs.JobValidateResponse) error { 290 defer metrics.MeasureSince([]string{"nomad", "job", "validate"}, time.Now()) 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 304 reply.DriverConfigValidated = true 305 return nil 306 } 307 308 // Revert is used to revert the job to a prior version 309 func (j *Job) Revert(args *structs.JobRevertRequest, reply *structs.JobRegisterResponse) error { 310 if done, err := j.srv.forward("Job.Revert", args, args, reply); done { 311 return err 312 } 313 defer metrics.MeasureSince([]string{"nomad", "job", "revert"}, time.Now()) 314 315 // Validate the arguments 316 if args.JobID == "" { 317 return fmt.Errorf("missing job ID for evaluation") 318 } 319 320 // Lookup the job by version 321 snap, err := j.srv.fsm.State().Snapshot() 322 if err != nil { 323 return err 324 } 325 326 ws := memdb.NewWatchSet() 327 cur, err := snap.JobByID(ws, args.JobID) 328 if err != nil { 329 return err 330 } 331 if cur == nil { 332 return fmt.Errorf("job %q not found", args.JobID) 333 } 334 if args.JobVersion == cur.Version { 335 return fmt.Errorf("can't revert to current version") 336 } 337 338 jobV, err := snap.JobByIDAndVersion(ws, args.JobID, args.JobVersion) 339 if err != nil { 340 return err 341 } 342 if jobV == nil { 343 return fmt.Errorf("job %q at version %d not found", args.JobID, args.JobVersion) 344 } 345 346 // Build the register request 347 reg := &structs.JobRegisterRequest{ 348 Job: jobV.Copy(), 349 WriteRequest: args.WriteRequest, 350 } 351 352 // If the request is enforcing the existing version do a check. 353 if args.EnforcePriorVersion != nil { 354 if cur.Version != *args.EnforcePriorVersion { 355 return fmt.Errorf("Current job has version %d; enforcing version %d", cur.Version, *args.EnforcePriorVersion) 356 } 357 358 reg.EnforceIndex = true 359 reg.JobModifyIndex = cur.JobModifyIndex 360 } 361 362 // Register the version. 363 return j.Register(reg, reply) 364 } 365 366 // Evaluate is used to force a job for re-evaluation 367 func (j *Job) Evaluate(args *structs.JobEvaluateRequest, reply *structs.JobRegisterResponse) error { 368 if done, err := j.srv.forward("Job.Evaluate", args, args, reply); done { 369 return err 370 } 371 defer metrics.MeasureSince([]string{"nomad", "job", "evaluate"}, time.Now()) 372 373 // Validate the arguments 374 if args.JobID == "" { 375 return fmt.Errorf("missing job ID for evaluation") 376 } 377 378 // Lookup the job 379 snap, err := j.srv.fsm.State().Snapshot() 380 if err != nil { 381 return err 382 } 383 ws := memdb.NewWatchSet() 384 job, err := snap.JobByID(ws, args.JobID) 385 if err != nil { 386 return err 387 } 388 if job == nil { 389 return fmt.Errorf("job not found") 390 } 391 392 if job.IsPeriodic() { 393 return fmt.Errorf("can't evaluate periodic job") 394 } else if job.IsParameterized() { 395 return fmt.Errorf("can't evaluate parameterized job") 396 } 397 398 // Create a new evaluation 399 eval := &structs.Evaluation{ 400 ID: structs.GenerateUUID(), 401 Priority: job.Priority, 402 Type: job.Type, 403 TriggeredBy: structs.EvalTriggerJobRegister, 404 JobID: job.ID, 405 JobModifyIndex: job.ModifyIndex, 406 Status: structs.EvalStatusPending, 407 } 408 update := &structs.EvalUpdateRequest{ 409 Evals: []*structs.Evaluation{eval}, 410 WriteRequest: structs.WriteRequest{Region: args.Region}, 411 } 412 413 // Commit this evaluation via Raft 414 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 415 if err != nil { 416 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 417 return err 418 } 419 420 // Setup the reply 421 reply.EvalID = eval.ID 422 reply.EvalCreateIndex = evalIndex 423 reply.JobModifyIndex = job.ModifyIndex 424 reply.Index = evalIndex 425 return nil 426 } 427 428 // Deregister is used to remove a job the cluster. 429 func (j *Job) Deregister(args *structs.JobDeregisterRequest, reply *structs.JobDeregisterResponse) error { 430 if done, err := j.srv.forward("Job.Deregister", args, args, reply); done { 431 return err 432 } 433 defer metrics.MeasureSince([]string{"nomad", "job", "deregister"}, time.Now()) 434 435 // Validate the arguments 436 if args.JobID == "" { 437 return fmt.Errorf("missing job ID for evaluation") 438 } 439 440 // Lookup the job 441 snap, err := j.srv.fsm.State().Snapshot() 442 if err != nil { 443 return err 444 } 445 ws := memdb.NewWatchSet() 446 job, err := snap.JobByID(ws, args.JobID) 447 if err != nil { 448 return err 449 } 450 451 // Commit this update via Raft 452 _, index, err := j.srv.raftApply(structs.JobDeregisterRequestType, args) 453 if err != nil { 454 j.srv.logger.Printf("[ERR] nomad.job: Deregister failed: %v", err) 455 return err 456 } 457 458 // Populate the reply with job information 459 reply.JobModifyIndex = index 460 461 // If the job is periodic or parameterized, we don't create an eval. 462 if job != nil && (job.IsPeriodic() || job.IsParameterized()) { 463 return nil 464 } 465 466 // Create a new evaluation 467 // XXX: The job priority / type is strange for this, since it's not a high 468 // priority even if the job was. The scheduler itself also doesn't matter, 469 // since all should be able to handle deregistration in the same way. 470 eval := &structs.Evaluation{ 471 ID: structs.GenerateUUID(), 472 Priority: structs.JobDefaultPriority, 473 Type: structs.JobTypeService, 474 TriggeredBy: structs.EvalTriggerJobDeregister, 475 JobID: args.JobID, 476 JobModifyIndex: index, 477 Status: structs.EvalStatusPending, 478 } 479 update := &structs.EvalUpdateRequest{ 480 Evals: []*structs.Evaluation{eval}, 481 WriteRequest: structs.WriteRequest{Region: args.Region}, 482 } 483 484 // Commit this evaluation via Raft 485 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 486 if err != nil { 487 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 488 return err 489 } 490 491 // Populate the reply with eval information 492 reply.EvalID = eval.ID 493 reply.EvalCreateIndex = evalIndex 494 reply.Index = evalIndex 495 return nil 496 } 497 498 // GetJob is used to request information about a specific job 499 func (j *Job) GetJob(args *structs.JobSpecificRequest, 500 reply *structs.SingleJobResponse) error { 501 if done, err := j.srv.forward("Job.GetJob", args, args, reply); done { 502 return err 503 } 504 defer metrics.MeasureSince([]string{"nomad", "job", "get_job"}, time.Now()) 505 506 // Setup the blocking query 507 opts := blockingOptions{ 508 queryOpts: &args.QueryOptions, 509 queryMeta: &reply.QueryMeta, 510 run: func(ws memdb.WatchSet, state *state.StateStore) error { 511 // Look for the job 512 out, err := state.JobByID(ws, args.JobID) 513 if err != nil { 514 return err 515 } 516 517 // Setup the output 518 reply.Job = out 519 if out != nil { 520 reply.Index = out.ModifyIndex 521 } else { 522 // Use the last index that affected the nodes table 523 index, err := state.Index("jobs") 524 if err != nil { 525 return err 526 } 527 reply.Index = index 528 } 529 530 // Set the query response 531 j.srv.setQueryMeta(&reply.QueryMeta) 532 return nil 533 }} 534 return j.srv.blockingRPC(&opts) 535 } 536 537 // GetJobVersions is used to retrieve all tracked versions of a job. 538 func (j *Job) GetJobVersions(args *structs.JobSpecificRequest, 539 reply *structs.JobVersionsResponse) error { 540 if done, err := j.srv.forward("Job.GetJobVersions", args, args, reply); done { 541 return err 542 } 543 defer metrics.MeasureSince([]string{"nomad", "job", "get_job_versions"}, time.Now()) 544 545 // Setup the blocking query 546 opts := blockingOptions{ 547 queryOpts: &args.QueryOptions, 548 queryMeta: &reply.QueryMeta, 549 run: func(ws memdb.WatchSet, state *state.StateStore) error { 550 // Look for the job 551 out, err := state.JobVersionsByID(ws, args.JobID) 552 if err != nil { 553 return err 554 } 555 556 // Setup the output 557 reply.Versions = out 558 if len(out) != 0 { 559 reply.Index = out[0].ModifyIndex 560 } else { 561 // Use the last index that affected the nodes table 562 index, err := state.Index("job_version") 563 if err != nil { 564 return err 565 } 566 reply.Index = index 567 } 568 569 // Set the query response 570 j.srv.setQueryMeta(&reply.QueryMeta) 571 return nil 572 }} 573 return j.srv.blockingRPC(&opts) 574 } 575 576 // List is used to list the jobs registered in the system 577 func (j *Job) List(args *structs.JobListRequest, 578 reply *structs.JobListResponse) error { 579 if done, err := j.srv.forward("Job.List", args, args, reply); done { 580 return err 581 } 582 defer metrics.MeasureSince([]string{"nomad", "job", "list"}, time.Now()) 583 584 // Setup the blocking query 585 opts := blockingOptions{ 586 queryOpts: &args.QueryOptions, 587 queryMeta: &reply.QueryMeta, 588 run: func(ws memdb.WatchSet, state *state.StateStore) error { 589 // Capture all the jobs 590 var err error 591 var iter memdb.ResultIterator 592 if prefix := args.QueryOptions.Prefix; prefix != "" { 593 iter, err = state.JobsByIDPrefix(ws, prefix) 594 } else { 595 iter, err = state.Jobs(ws) 596 } 597 if err != nil { 598 return err 599 } 600 601 var jobs []*structs.JobListStub 602 for { 603 raw := iter.Next() 604 if raw == nil { 605 break 606 } 607 job := raw.(*structs.Job) 608 summary, err := state.JobSummaryByID(ws, job.ID) 609 if err != nil { 610 return fmt.Errorf("unable to look up summary for job: %v", job.ID) 611 } 612 jobs = append(jobs, job.Stub(summary)) 613 } 614 reply.Jobs = jobs 615 616 // Use the last index that affected the jobs table 617 index, err := state.Index("jobs") 618 if err != nil { 619 return err 620 } 621 reply.Index = index 622 623 // Set the query response 624 j.srv.setQueryMeta(&reply.QueryMeta) 625 return nil 626 }} 627 return j.srv.blockingRPC(&opts) 628 } 629 630 // Allocations is used to list the allocations for a job 631 func (j *Job) Allocations(args *structs.JobSpecificRequest, 632 reply *structs.JobAllocationsResponse) error { 633 if done, err := j.srv.forward("Job.Allocations", args, args, reply); done { 634 return err 635 } 636 defer metrics.MeasureSince([]string{"nomad", "job", "allocations"}, time.Now()) 637 638 // Setup the blocking query 639 opts := blockingOptions{ 640 queryOpts: &args.QueryOptions, 641 queryMeta: &reply.QueryMeta, 642 run: func(ws memdb.WatchSet, state *state.StateStore) error { 643 // Capture the allocations 644 allocs, err := state.AllocsByJob(ws, args.JobID, args.AllAllocs) 645 if err != nil { 646 return err 647 } 648 649 // Convert to stubs 650 if len(allocs) > 0 { 651 reply.Allocations = make([]*structs.AllocListStub, 0, len(allocs)) 652 for _, alloc := range allocs { 653 reply.Allocations = append(reply.Allocations, alloc.Stub()) 654 } 655 } 656 657 // Use the last index that affected the allocs table 658 index, err := state.Index("allocs") 659 if err != nil { 660 return err 661 } 662 reply.Index = index 663 664 // Set the query response 665 j.srv.setQueryMeta(&reply.QueryMeta) 666 return nil 667 668 }} 669 return j.srv.blockingRPC(&opts) 670 } 671 672 // Evaluations is used to list the evaluations for a job 673 func (j *Job) Evaluations(args *structs.JobSpecificRequest, 674 reply *structs.JobEvaluationsResponse) error { 675 if done, err := j.srv.forward("Job.Evaluations", args, args, reply); done { 676 return err 677 } 678 defer metrics.MeasureSince([]string{"nomad", "job", "evaluations"}, time.Now()) 679 680 // Setup the blocking query 681 opts := blockingOptions{ 682 queryOpts: &args.QueryOptions, 683 queryMeta: &reply.QueryMeta, 684 run: func(ws memdb.WatchSet, state *state.StateStore) error { 685 // Capture the evals 686 var err error 687 reply.Evaluations, err = state.EvalsByJob(ws, args.JobID) 688 if err != nil { 689 return err 690 } 691 692 // Use the last index that affected the evals table 693 index, err := state.Index("evals") 694 if err != nil { 695 return err 696 } 697 reply.Index = index 698 699 // Set the query response 700 j.srv.setQueryMeta(&reply.QueryMeta) 701 return nil 702 }} 703 704 return j.srv.blockingRPC(&opts) 705 } 706 707 // Plan is used to cause a dry-run evaluation of the Job and return the results 708 // with a potential diff containing annotations. 709 func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse) error { 710 if done, err := j.srv.forward("Job.Plan", args, args, reply); done { 711 return err 712 } 713 defer metrics.MeasureSince([]string{"nomad", "job", "plan"}, time.Now()) 714 715 // Validate the arguments 716 if args.Job == nil { 717 return fmt.Errorf("Job required for plan") 718 } 719 720 // Initialize the job fields (sets defaults and any necessary init work). 721 args.Job.Canonicalize() 722 723 // Add implicit constraints 724 setImplicitConstraints(args.Job) 725 726 // Validate the job. 727 if err := validateJob(args.Job); err != nil { 728 return err 729 } 730 731 // Acquire a snapshot of the state 732 snap, err := j.srv.fsm.State().Snapshot() 733 if err != nil { 734 return err 735 } 736 737 // Get the original job 738 ws := memdb.NewWatchSet() 739 oldJob, err := snap.JobByID(ws, args.Job.ID) 740 if err != nil { 741 return err 742 } 743 744 var index uint64 745 var updatedIndex uint64 746 if oldJob != nil { 747 index = oldJob.JobModifyIndex 748 updatedIndex = oldJob.JobModifyIndex + 1 749 } 750 751 // Insert the updated Job into the snapshot 752 snap.UpsertJob(updatedIndex, args.Job) 753 754 // Create an eval and mark it as requiring annotations and insert that as well 755 eval := &structs.Evaluation{ 756 ID: structs.GenerateUUID(), 757 Priority: args.Job.Priority, 758 Type: args.Job.Type, 759 TriggeredBy: structs.EvalTriggerJobRegister, 760 JobID: args.Job.ID, 761 JobModifyIndex: updatedIndex, 762 Status: structs.EvalStatusPending, 763 AnnotatePlan: true, 764 } 765 766 // Create an in-memory Planner that returns no errors and stores the 767 // submitted plan and created evals. 768 planner := &scheduler.Harness{ 769 State: &snap.StateStore, 770 } 771 772 // Create the scheduler and run it 773 sched, err := scheduler.NewScheduler(eval.Type, j.srv.logger, snap, planner) 774 if err != nil { 775 return err 776 } 777 778 if err := sched.Process(eval); err != nil { 779 return err 780 } 781 782 // Annotate and store the diff 783 if plans := len(planner.Plans); plans != 1 { 784 return fmt.Errorf("scheduler resulted in an unexpected number of plans: %v", plans) 785 } 786 annotations := planner.Plans[0].Annotations 787 if args.Diff { 788 jobDiff, err := oldJob.Diff(args.Job, true) 789 if err != nil { 790 return fmt.Errorf("failed to create job diff: %v", err) 791 } 792 793 if err := scheduler.Annotate(jobDiff, annotations); err != nil { 794 return fmt.Errorf("failed to annotate job diff: %v", err) 795 } 796 reply.Diff = jobDiff 797 } 798 799 // Grab the failures 800 if len(planner.Evals) != 1 { 801 return fmt.Errorf("scheduler resulted in an unexpected number of eval updates: %v", planner.Evals) 802 } 803 updatedEval := planner.Evals[0] 804 805 // If it is a periodic job calculate the next launch 806 if args.Job.IsPeriodic() && args.Job.Periodic.Enabled { 807 reply.NextPeriodicLaunch = args.Job.Periodic.Next(time.Now().In(args.Job.Periodic.GetLocation())) 808 } 809 810 reply.FailedTGAllocs = updatedEval.FailedTGAllocs 811 reply.JobModifyIndex = index 812 reply.Annotations = annotations 813 reply.CreatedEvals = planner.CreateEvals 814 reply.Index = index 815 return nil 816 } 817 818 // validateJob validates a Job and task drivers and returns an error if there is 819 // a validation problem or if the Job is of a type a user is not allowed to 820 // submit. 821 func validateJob(job *structs.Job) error { 822 validationErrors := new(multierror.Error) 823 if err := job.Validate(); err != nil { 824 multierror.Append(validationErrors, err) 825 } 826 827 // Get the signals required 828 signals := job.RequiredSignals() 829 830 // Validate the driver configurations. 831 for _, tg := range job.TaskGroups { 832 // Get the signals for the task group 833 tgSignals, tgOk := signals[tg.Name] 834 835 for _, task := range tg.Tasks { 836 d, err := driver.NewDriver( 837 task.Driver, 838 driver.NewEmptyDriverContext(), 839 ) 840 if err != nil { 841 msg := "failed to create driver for task %q in group %q for validation: %v" 842 multierror.Append(validationErrors, fmt.Errorf(msg, tg.Name, task.Name, err)) 843 continue 844 } 845 846 if err := d.Validate(task.Config); err != nil { 847 formatted := fmt.Errorf("group %q -> task %q -> config: %v", tg.Name, task.Name, err) 848 multierror.Append(validationErrors, formatted) 849 } 850 851 // The task group didn't have any task that required signals 852 if !tgOk { 853 continue 854 } 855 856 // This task requires signals. Ensure the driver is capable 857 if required, ok := tgSignals[task.Name]; ok { 858 abilities := d.Abilities() 859 if !abilities.SendSignals { 860 formatted := fmt.Errorf("group %q -> task %q: driver %q doesn't support sending signals. Requested signals are %v", 861 tg.Name, task.Name, task.Driver, strings.Join(required, ", ")) 862 multierror.Append(validationErrors, formatted) 863 } 864 } 865 } 866 } 867 868 if job.Type == structs.JobTypeCore { 869 multierror.Append(validationErrors, fmt.Errorf("job type cannot be core")) 870 } 871 872 if len(job.Payload) != 0 { 873 multierror.Append(validationErrors, fmt.Errorf("job can't be submitted with a payload, only dispatched")) 874 } 875 876 return validationErrors.ErrorOrNil() 877 } 878 879 // Dispatch a parameterized job. 880 func (j *Job) Dispatch(args *structs.JobDispatchRequest, reply *structs.JobDispatchResponse) error { 881 if done, err := j.srv.forward("Job.Dispatch", args, args, reply); done { 882 return err 883 } 884 defer metrics.MeasureSince([]string{"nomad", "job", "dispatch"}, time.Now()) 885 886 // Lookup the parameterized job 887 if args.JobID == "" { 888 return fmt.Errorf("missing parameterized job ID") 889 } 890 891 snap, err := j.srv.fsm.State().Snapshot() 892 if err != nil { 893 return err 894 } 895 ws := memdb.NewWatchSet() 896 parameterizedJob, err := snap.JobByID(ws, args.JobID) 897 if err != nil { 898 return err 899 } 900 if parameterizedJob == nil { 901 return fmt.Errorf("parameterized job not found") 902 } 903 904 if !parameterizedJob.IsParameterized() { 905 return fmt.Errorf("Specified job %q is not a parameterized job", args.JobID) 906 } 907 908 if parameterizedJob.Stop { 909 return fmt.Errorf("Specified job %q is stopped", args.JobID) 910 } 911 912 // Validate the arguments 913 if err := validateDispatchRequest(args, parameterizedJob); err != nil { 914 return err 915 } 916 917 // Derive the child job and commit it via Raft 918 dispatchJob := parameterizedJob.Copy() 919 dispatchJob.ParameterizedJob = nil 920 dispatchJob.ID = structs.DispatchedID(parameterizedJob.ID, time.Now()) 921 dispatchJob.ParentID = parameterizedJob.ID 922 dispatchJob.Name = dispatchJob.ID 923 924 // Merge in the meta data 925 for k, v := range args.Meta { 926 if dispatchJob.Meta == nil { 927 dispatchJob.Meta = make(map[string]string, len(args.Meta)) 928 } 929 dispatchJob.Meta[k] = v 930 } 931 932 // Compress the payload 933 dispatchJob.Payload = snappy.Encode(nil, args.Payload) 934 935 regReq := &structs.JobRegisterRequest{ 936 Job: dispatchJob, 937 WriteRequest: args.WriteRequest, 938 } 939 940 // Commit this update via Raft 941 _, jobCreateIndex, err := j.srv.raftApply(structs.JobRegisterRequestType, regReq) 942 if err != nil { 943 j.srv.logger.Printf("[ERR] nomad.job: Dispatched job register failed: %v", err) 944 return err 945 } 946 947 reply.JobCreateIndex = jobCreateIndex 948 reply.DispatchedJobID = dispatchJob.ID 949 reply.Index = jobCreateIndex 950 951 // If the job is periodic, we don't create an eval. 952 if !dispatchJob.IsPeriodic() { 953 // Create a new evaluation 954 eval := &structs.Evaluation{ 955 ID: structs.GenerateUUID(), 956 Priority: dispatchJob.Priority, 957 Type: dispatchJob.Type, 958 TriggeredBy: structs.EvalTriggerJobRegister, 959 JobID: dispatchJob.ID, 960 JobModifyIndex: jobCreateIndex, 961 Status: structs.EvalStatusPending, 962 } 963 update := &structs.EvalUpdateRequest{ 964 Evals: []*structs.Evaluation{eval}, 965 WriteRequest: structs.WriteRequest{Region: args.Region}, 966 } 967 968 // Commit this evaluation via Raft 969 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 970 if err != nil { 971 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 972 return err 973 } 974 975 // Setup the reply 976 reply.EvalID = eval.ID 977 reply.EvalCreateIndex = evalIndex 978 reply.Index = evalIndex 979 } 980 981 return nil 982 } 983 984 // validateDispatchRequest returns whether the request is valid given the 985 // parameterized job. 986 func validateDispatchRequest(req *structs.JobDispatchRequest, job *structs.Job) error { 987 // Check the payload constraint is met 988 hasInputData := len(req.Payload) != 0 989 if job.ParameterizedJob.Payload == structs.DispatchPayloadRequired && !hasInputData { 990 return fmt.Errorf("Payload is not provided but required by parameterized job") 991 } else if job.ParameterizedJob.Payload == structs.DispatchPayloadForbidden && hasInputData { 992 return fmt.Errorf("Payload provided but forbidden by parameterized job") 993 } 994 995 // Check the payload doesn't exceed the size limit 996 if l := len(req.Payload); l > DispatchPayloadSizeLimit { 997 return fmt.Errorf("Payload exceeds maximum size; %d > %d", l, DispatchPayloadSizeLimit) 998 } 999 1000 // Check if the metadata is a set 1001 keys := make(map[string]struct{}, len(req.Meta)) 1002 for k := range keys { 1003 if _, ok := keys[k]; ok { 1004 return fmt.Errorf("Duplicate key %q in passed metadata", k) 1005 } 1006 keys[k] = struct{}{} 1007 } 1008 1009 required := helper.SliceStringToSet(job.ParameterizedJob.MetaRequired) 1010 optional := helper.SliceStringToSet(job.ParameterizedJob.MetaOptional) 1011 1012 // Check the metadata key constraints are met 1013 unpermitted := make(map[string]struct{}) 1014 for k := range req.Meta { 1015 _, req := required[k] 1016 _, opt := optional[k] 1017 if !req && !opt { 1018 unpermitted[k] = struct{}{} 1019 } 1020 } 1021 1022 if len(unpermitted) != 0 { 1023 flat := make([]string, 0, len(unpermitted)) 1024 for k := range unpermitted { 1025 flat = append(flat, k) 1026 } 1027 1028 return fmt.Errorf("Dispatch request included unpermitted metadata keys: %v", flat) 1029 } 1030 1031 missing := make(map[string]struct{}) 1032 for _, k := range job.ParameterizedJob.MetaRequired { 1033 if _, ok := req.Meta[k]; !ok { 1034 missing[k] = struct{}{} 1035 } 1036 } 1037 1038 if len(missing) != 0 { 1039 flat := make([]string, 0, len(missing)) 1040 for k := range missing { 1041 flat = append(flat, k) 1042 } 1043 1044 return fmt.Errorf("Dispatch did not provide required meta keys: %v", flat) 1045 } 1046 1047 return nil 1048 }