github.com/jrxfive/nomad@v0.6.1-0.20170802162750-1fef470e89bf/nomad/job_endpoint.go (about) 1 package nomad 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strings" 8 "time" 9 10 "github.com/armon/go-metrics" 11 "github.com/golang/snappy" 12 "github.com/hashicorp/consul/lib" 13 "github.com/hashicorp/go-memdb" 14 "github.com/hashicorp/go-multierror" 15 "github.com/hashicorp/nomad/client/driver" 16 "github.com/hashicorp/nomad/helper" 17 "github.com/hashicorp/nomad/nomad/state" 18 "github.com/hashicorp/nomad/nomad/structs" 19 "github.com/hashicorp/nomad/scheduler" 20 ) 21 22 const ( 23 // RegisterEnforceIndexErrPrefix is the prefix to use in errors caused by 24 // enforcing the job modify index during registers. 25 RegisterEnforceIndexErrPrefix = "Enforcing job modify index" 26 27 // DispatchPayloadSizeLimit is the maximum size of the uncompressed input 28 // data payload. 29 DispatchPayloadSizeLimit = 16 * 1024 30 ) 31 32 var ( 33 // vaultConstraint is the implicit constraint added to jobs requesting a 34 // Vault token 35 vaultConstraint = &structs.Constraint{ 36 LTarget: "${attr.vault.version}", 37 RTarget: ">= 0.6.1", 38 Operand: structs.ConstraintVersion, 39 } 40 ) 41 42 // Job endpoint is used for job interactions 43 type Job struct { 44 srv *Server 45 } 46 47 // Register is used to upsert a job for scheduling 48 func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegisterResponse) error { 49 if done, err := j.srv.forward("Job.Register", args, args, reply); done { 50 return err 51 } 52 defer metrics.MeasureSince([]string{"nomad", "job", "register"}, time.Now()) 53 54 // Validate the arguments 55 if args.Job == nil { 56 return fmt.Errorf("missing job for registration") 57 } 58 59 // Initialize the job fields (sets defaults and any necessary init work). 60 canonicalizeWarnings := args.Job.Canonicalize() 61 62 // Add implicit constraints 63 setImplicitConstraints(args.Job) 64 65 // Validate the job and capture any warnings 66 err, warnings := validateJob(args.Job) 67 if err != nil { 68 return err 69 } 70 71 // Set the warning message 72 reply.Warnings = structs.MergeMultierrorWarnings(warnings, canonicalizeWarnings) 73 74 // Lookup the job 75 snap, err := j.srv.fsm.State().Snapshot() 76 if err != nil { 77 return err 78 } 79 ws := memdb.NewWatchSet() 80 existingJob, err := snap.JobByID(ws, args.Job.ID) 81 if err != nil { 82 return err 83 } 84 85 // If EnforceIndex set, check it before trying to apply 86 if args.EnforceIndex { 87 jmi := args.JobModifyIndex 88 if existingJob != nil { 89 if jmi == 0 { 90 return fmt.Errorf("%s 0: job already exists", RegisterEnforceIndexErrPrefix) 91 } else if jmi != existingJob.JobModifyIndex { 92 return fmt.Errorf("%s %d: job exists with conflicting job modify index: %d", 93 RegisterEnforceIndexErrPrefix, jmi, existingJob.JobModifyIndex) 94 } 95 } else if jmi != 0 { 96 return fmt.Errorf("%s %d: job does not exist", RegisterEnforceIndexErrPrefix, jmi) 97 } 98 } 99 100 // Validate job transitions if its an update 101 if existingJob != nil { 102 if err := validateJobUpdate(existingJob, args.Job); err != nil { 103 return err 104 } 105 } 106 107 // Ensure that the job has permissions for the requested Vault tokens 108 policies := args.Job.VaultPolicies() 109 if len(policies) != 0 { 110 vconf := j.srv.config.VaultConfig 111 if !vconf.IsEnabled() { 112 return fmt.Errorf("Vault not enabled and Vault policies requested") 113 } 114 115 // Have to check if the user has permissions 116 if !vconf.AllowsUnauthenticated() { 117 if args.Job.VaultToken == "" { 118 return fmt.Errorf("Vault policies requested but missing Vault Token") 119 } 120 121 vault := j.srv.vault 122 s, err := vault.LookupToken(context.Background(), args.Job.VaultToken) 123 if err != nil { 124 return err 125 } 126 127 allowedPolicies, err := PoliciesFrom(s) 128 if err != nil { 129 return err 130 } 131 132 // If we are given a root token it can access all policies 133 if !lib.StrContains(allowedPolicies, "root") { 134 flatPolicies := structs.VaultPoliciesSet(policies) 135 subset, offending := helper.SliceStringIsSubset(allowedPolicies, flatPolicies) 136 if !subset { 137 return fmt.Errorf("Passed Vault Token doesn't allow access to the following policies: %s", 138 strings.Join(offending, ", ")) 139 } 140 } 141 } 142 } 143 144 // Clear the Vault token 145 args.Job.VaultToken = "" 146 147 // Check if the job has changed at all 148 if existingJob == nil || existingJob.SpecChanged(args.Job) { 149 // Set the submit time 150 args.Job.SetSubmitTime() 151 152 // Commit this update via Raft 153 _, index, err := j.srv.raftApply(structs.JobRegisterRequestType, args) 154 if err != nil { 155 j.srv.logger.Printf("[ERR] nomad.job: Register failed: %v", err) 156 return err 157 } 158 159 // Populate the reply with job information 160 reply.JobModifyIndex = index 161 } else { 162 reply.JobModifyIndex = existingJob.JobModifyIndex 163 } 164 165 // If the job is periodic or parameterized, we don't create an eval. 166 if args.Job.IsPeriodic() || args.Job.IsParameterized() { 167 return nil 168 } 169 170 // Create a new evaluation 171 eval := &structs.Evaluation{ 172 ID: structs.GenerateUUID(), 173 Priority: args.Job.Priority, 174 Type: args.Job.Type, 175 TriggeredBy: structs.EvalTriggerJobRegister, 176 JobID: args.Job.ID, 177 JobModifyIndex: reply.JobModifyIndex, 178 Status: structs.EvalStatusPending, 179 } 180 update := &structs.EvalUpdateRequest{ 181 Evals: []*structs.Evaluation{eval}, 182 WriteRequest: structs.WriteRequest{Region: args.Region}, 183 } 184 185 // Commit this evaluation via Raft 186 // XXX: There is a risk of partial failure where the JobRegister succeeds 187 // but that the EvalUpdate does not. 188 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 189 if err != nil { 190 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 191 return err 192 } 193 194 // Populate the reply with eval information 195 reply.EvalID = eval.ID 196 reply.EvalCreateIndex = evalIndex 197 reply.Index = evalIndex 198 return nil 199 } 200 201 // setImplicitConstraints adds implicit constraints to the job based on the 202 // features it is requesting. 203 func setImplicitConstraints(j *structs.Job) { 204 // Get the required Vault Policies 205 policies := j.VaultPolicies() 206 207 // Get the required signals 208 signals := j.RequiredSignals() 209 210 // Hot path 211 if len(signals) == 0 && len(policies) == 0 { 212 return 213 } 214 215 // Add Vault constraints 216 for _, tg := range j.TaskGroups { 217 _, ok := policies[tg.Name] 218 if !ok { 219 // Not requesting Vault 220 continue 221 } 222 223 found := false 224 for _, c := range tg.Constraints { 225 if c.Equal(vaultConstraint) { 226 found = true 227 break 228 } 229 } 230 231 if !found { 232 tg.Constraints = append(tg.Constraints, vaultConstraint) 233 } 234 } 235 236 // Add signal constraints 237 for _, tg := range j.TaskGroups { 238 tgSignals, ok := signals[tg.Name] 239 if !ok { 240 // Not requesting Vault 241 continue 242 } 243 244 // Flatten the signals 245 required := helper.MapStringStringSliceValueSet(tgSignals) 246 sigConstraint := getSignalConstraint(required) 247 248 found := false 249 for _, c := range tg.Constraints { 250 if c.Equal(sigConstraint) { 251 found = true 252 break 253 } 254 } 255 256 if !found { 257 tg.Constraints = append(tg.Constraints, sigConstraint) 258 } 259 } 260 } 261 262 // getSignalConstraint builds a suitable constraint based on the required 263 // signals 264 func getSignalConstraint(signals []string) *structs.Constraint { 265 return &structs.Constraint{ 266 Operand: structs.ConstraintSetContains, 267 LTarget: "${attr.os.signals}", 268 RTarget: strings.Join(signals, ","), 269 } 270 } 271 272 // Summary retreives the summary of a job 273 func (j *Job) Summary(args *structs.JobSummaryRequest, 274 reply *structs.JobSummaryResponse) error { 275 if done, err := j.srv.forward("Job.Summary", args, args, reply); done { 276 return err 277 } 278 defer metrics.MeasureSince([]string{"nomad", "job_summary", "get_job_summary"}, time.Now()) 279 // Setup the blocking query 280 opts := blockingOptions{ 281 queryOpts: &args.QueryOptions, 282 queryMeta: &reply.QueryMeta, 283 run: func(ws memdb.WatchSet, state *state.StateStore) error { 284 // Look for job summary 285 out, err := state.JobSummaryByID(ws, args.JobID) 286 if err != nil { 287 return err 288 } 289 290 // Setup the output 291 reply.JobSummary = out 292 if out != nil { 293 reply.Index = out.ModifyIndex 294 } else { 295 // Use the last index that affected the job_summary table 296 index, err := state.Index("job_summary") 297 if err != nil { 298 return err 299 } 300 reply.Index = index 301 } 302 303 // Set the query response 304 j.srv.setQueryMeta(&reply.QueryMeta) 305 return nil 306 }} 307 return j.srv.blockingRPC(&opts) 308 } 309 310 // Validate validates a job 311 func (j *Job) Validate(args *structs.JobValidateRequest, reply *structs.JobValidateResponse) error { 312 defer metrics.MeasureSince([]string{"nomad", "job", "validate"}, time.Now()) 313 314 // Initialize the job fields (sets defaults and any necessary init work). 315 canonicalizeWarnings := args.Job.Canonicalize() 316 317 // Add implicit constraints 318 setImplicitConstraints(args.Job) 319 320 // Validate the job and capture any warnings 321 err, warnings := validateJob(args.Job) 322 if err != nil { 323 if merr, ok := err.(*multierror.Error); ok { 324 for _, err := range merr.Errors { 325 reply.ValidationErrors = append(reply.ValidationErrors, err.Error()) 326 } 327 reply.Error = merr.Error() 328 } else { 329 reply.ValidationErrors = append(reply.ValidationErrors, err.Error()) 330 reply.Error = err.Error() 331 } 332 } 333 334 // Set the warning message 335 reply.Warnings = structs.MergeMultierrorWarnings(warnings, canonicalizeWarnings) 336 reply.DriverConfigValidated = true 337 return nil 338 } 339 340 // Revert is used to revert the job to a prior version 341 func (j *Job) Revert(args *structs.JobRevertRequest, reply *structs.JobRegisterResponse) error { 342 if done, err := j.srv.forward("Job.Revert", args, args, reply); done { 343 return err 344 } 345 defer metrics.MeasureSince([]string{"nomad", "job", "revert"}, time.Now()) 346 347 // Validate the arguments 348 if args.JobID == "" { 349 return fmt.Errorf("missing job ID for revert") 350 } 351 352 // Lookup the job by version 353 snap, err := j.srv.fsm.State().Snapshot() 354 if err != nil { 355 return err 356 } 357 358 ws := memdb.NewWatchSet() 359 cur, err := snap.JobByID(ws, args.JobID) 360 if err != nil { 361 return err 362 } 363 if cur == nil { 364 return fmt.Errorf("job %q not found", args.JobID) 365 } 366 if args.JobVersion == cur.Version { 367 return fmt.Errorf("can't revert to current version") 368 } 369 370 jobV, err := snap.JobByIDAndVersion(ws, args.JobID, args.JobVersion) 371 if err != nil { 372 return err 373 } 374 if jobV == nil { 375 return fmt.Errorf("job %q at version %d not found", args.JobID, args.JobVersion) 376 } 377 378 // Build the register request 379 reg := &structs.JobRegisterRequest{ 380 Job: jobV.Copy(), 381 WriteRequest: args.WriteRequest, 382 } 383 384 // If the request is enforcing the existing version do a check. 385 if args.EnforcePriorVersion != nil { 386 if cur.Version != *args.EnforcePriorVersion { 387 return fmt.Errorf("Current job has version %d; enforcing version %d", cur.Version, *args.EnforcePriorVersion) 388 } 389 390 reg.EnforceIndex = true 391 reg.JobModifyIndex = cur.JobModifyIndex 392 } 393 394 // Register the version. 395 return j.Register(reg, reply) 396 } 397 398 // Stable is used to mark the job version as stable 399 func (j *Job) Stable(args *structs.JobStabilityRequest, reply *structs.JobStabilityResponse) error { 400 if done, err := j.srv.forward("Job.Stable", args, args, reply); done { 401 return err 402 } 403 defer metrics.MeasureSince([]string{"nomad", "job", "stable"}, time.Now()) 404 405 // Validate the arguments 406 if args.JobID == "" { 407 return fmt.Errorf("missing job ID for marking job as stable") 408 } 409 410 // Lookup the job by version 411 snap, err := j.srv.fsm.State().Snapshot() 412 if err != nil { 413 return err 414 } 415 416 ws := memdb.NewWatchSet() 417 jobV, err := snap.JobByIDAndVersion(ws, args.JobID, args.JobVersion) 418 if err != nil { 419 return err 420 } 421 if jobV == nil { 422 return fmt.Errorf("job %q at version %d not found", args.JobID, args.JobVersion) 423 } 424 425 // Commit this stability request via Raft 426 _, modifyIndex, err := j.srv.raftApply(structs.JobStabilityRequestType, args) 427 if err != nil { 428 j.srv.logger.Printf("[ERR] nomad.job: Job stability request failed: %v", err) 429 return err 430 } 431 432 // Setup the reply 433 reply.Index = modifyIndex 434 return nil 435 } 436 437 // Evaluate is used to force a job for re-evaluation 438 func (j *Job) Evaluate(args *structs.JobEvaluateRequest, reply *structs.JobRegisterResponse) error { 439 if done, err := j.srv.forward("Job.Evaluate", args, args, reply); done { 440 return err 441 } 442 defer metrics.MeasureSince([]string{"nomad", "job", "evaluate"}, time.Now()) 443 444 // Validate the arguments 445 if args.JobID == "" { 446 return fmt.Errorf("missing job ID for evaluation") 447 } 448 449 // Lookup the job 450 snap, err := j.srv.fsm.State().Snapshot() 451 if err != nil { 452 return err 453 } 454 ws := memdb.NewWatchSet() 455 job, err := snap.JobByID(ws, args.JobID) 456 if err != nil { 457 return err 458 } 459 if job == nil { 460 return fmt.Errorf("job not found") 461 } 462 463 if job.IsPeriodic() { 464 return fmt.Errorf("can't evaluate periodic job") 465 } else if job.IsParameterized() { 466 return fmt.Errorf("can't evaluate parameterized job") 467 } 468 469 // Create a new evaluation 470 eval := &structs.Evaluation{ 471 ID: structs.GenerateUUID(), 472 Priority: job.Priority, 473 Type: job.Type, 474 TriggeredBy: structs.EvalTriggerJobRegister, 475 JobID: job.ID, 476 JobModifyIndex: job.ModifyIndex, 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 // Setup the reply 492 reply.EvalID = eval.ID 493 reply.EvalCreateIndex = evalIndex 494 reply.JobModifyIndex = job.ModifyIndex 495 reply.Index = evalIndex 496 return nil 497 } 498 499 // Deregister is used to remove a job the cluster. 500 func (j *Job) Deregister(args *structs.JobDeregisterRequest, reply *structs.JobDeregisterResponse) error { 501 if done, err := j.srv.forward("Job.Deregister", args, args, reply); done { 502 return err 503 } 504 defer metrics.MeasureSince([]string{"nomad", "job", "deregister"}, time.Now()) 505 506 // Validate the arguments 507 if args.JobID == "" { 508 return fmt.Errorf("missing job ID for deregistering") 509 } 510 511 // Lookup the job 512 snap, err := j.srv.fsm.State().Snapshot() 513 if err != nil { 514 return err 515 } 516 ws := memdb.NewWatchSet() 517 job, err := snap.JobByID(ws, args.JobID) 518 if err != nil { 519 return err 520 } 521 522 // Commit this update via Raft 523 _, index, err := j.srv.raftApply(structs.JobDeregisterRequestType, args) 524 if err != nil { 525 j.srv.logger.Printf("[ERR] nomad.job: Deregister failed: %v", err) 526 return err 527 } 528 529 // Populate the reply with job information 530 reply.JobModifyIndex = index 531 532 // If the job is periodic or parameterized, we don't create an eval. 533 if job != nil && (job.IsPeriodic() || job.IsParameterized()) { 534 return nil 535 } 536 537 // Create a new evaluation 538 // XXX: The job priority / type is strange for this, since it's not a high 539 // priority even if the job was. The scheduler itself also doesn't matter, 540 // since all should be able to handle deregistration in the same way. 541 eval := &structs.Evaluation{ 542 ID: structs.GenerateUUID(), 543 Priority: structs.JobDefaultPriority, 544 Type: structs.JobTypeService, 545 TriggeredBy: structs.EvalTriggerJobDeregister, 546 JobID: args.JobID, 547 JobModifyIndex: index, 548 Status: structs.EvalStatusPending, 549 } 550 update := &structs.EvalUpdateRequest{ 551 Evals: []*structs.Evaluation{eval}, 552 WriteRequest: structs.WriteRequest{Region: args.Region}, 553 } 554 555 // Commit this evaluation via Raft 556 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 557 if err != nil { 558 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 559 return err 560 } 561 562 // Populate the reply with eval information 563 reply.EvalID = eval.ID 564 reply.EvalCreateIndex = evalIndex 565 reply.Index = evalIndex 566 return nil 567 } 568 569 // GetJob is used to request information about a specific job 570 func (j *Job) GetJob(args *structs.JobSpecificRequest, 571 reply *structs.SingleJobResponse) error { 572 if done, err := j.srv.forward("Job.GetJob", args, args, reply); done { 573 return err 574 } 575 defer metrics.MeasureSince([]string{"nomad", "job", "get_job"}, time.Now()) 576 577 // Setup the blocking query 578 opts := blockingOptions{ 579 queryOpts: &args.QueryOptions, 580 queryMeta: &reply.QueryMeta, 581 run: func(ws memdb.WatchSet, state *state.StateStore) error { 582 // Look for the job 583 out, err := state.JobByID(ws, args.JobID) 584 if err != nil { 585 return err 586 } 587 588 // Setup the output 589 reply.Job = out 590 if out != nil { 591 reply.Index = out.ModifyIndex 592 } else { 593 // Use the last index that affected the nodes table 594 index, err := state.Index("jobs") 595 if err != nil { 596 return err 597 } 598 reply.Index = index 599 } 600 601 // Set the query response 602 j.srv.setQueryMeta(&reply.QueryMeta) 603 return nil 604 }} 605 return j.srv.blockingRPC(&opts) 606 } 607 608 // GetJobVersions is used to retrieve all tracked versions of a job. 609 func (j *Job) GetJobVersions(args *structs.JobVersionsRequest, 610 reply *structs.JobVersionsResponse) error { 611 if done, err := j.srv.forward("Job.GetJobVersions", args, args, reply); done { 612 return err 613 } 614 defer metrics.MeasureSince([]string{"nomad", "job", "get_job_versions"}, time.Now()) 615 616 // Setup the blocking query 617 opts := blockingOptions{ 618 queryOpts: &args.QueryOptions, 619 queryMeta: &reply.QueryMeta, 620 run: func(ws memdb.WatchSet, state *state.StateStore) error { 621 // Look for the job 622 out, err := state.JobVersionsByID(ws, args.JobID) 623 if err != nil { 624 return err 625 } 626 627 // Setup the output 628 reply.Versions = out 629 if len(out) != 0 { 630 reply.Index = out[0].ModifyIndex 631 632 // Compute the diffs 633 if args.Diffs { 634 for i := 0; i < len(out)-1; i++ { 635 old, new := out[i+1], out[i] 636 d, err := old.Diff(new, true) 637 if err != nil { 638 return fmt.Errorf("failed to create job diff: %v", err) 639 } 640 reply.Diffs = append(reply.Diffs, d) 641 } 642 } 643 } else { 644 // Use the last index that affected the nodes table 645 index, err := state.Index("job_version") 646 if err != nil { 647 return err 648 } 649 reply.Index = index 650 } 651 652 // Set the query response 653 j.srv.setQueryMeta(&reply.QueryMeta) 654 return nil 655 }} 656 return j.srv.blockingRPC(&opts) 657 } 658 659 // List is used to list the jobs registered in the system 660 func (j *Job) List(args *structs.JobListRequest, 661 reply *structs.JobListResponse) error { 662 if done, err := j.srv.forward("Job.List", args, args, reply); done { 663 return err 664 } 665 defer metrics.MeasureSince([]string{"nomad", "job", "list"}, time.Now()) 666 667 // Setup the blocking query 668 opts := blockingOptions{ 669 queryOpts: &args.QueryOptions, 670 queryMeta: &reply.QueryMeta, 671 run: func(ws memdb.WatchSet, state *state.StateStore) error { 672 // Capture all the jobs 673 var err error 674 var iter memdb.ResultIterator 675 if prefix := args.QueryOptions.Prefix; prefix != "" { 676 iter, err = state.JobsByIDPrefix(ws, prefix) 677 } else { 678 iter, err = state.Jobs(ws) 679 } 680 if err != nil { 681 return err 682 } 683 684 var jobs []*structs.JobListStub 685 for { 686 raw := iter.Next() 687 if raw == nil { 688 break 689 } 690 job := raw.(*structs.Job) 691 summary, err := state.JobSummaryByID(ws, job.ID) 692 if err != nil { 693 return fmt.Errorf("unable to look up summary for job: %v", job.ID) 694 } 695 jobs = append(jobs, job.Stub(summary)) 696 } 697 reply.Jobs = jobs 698 699 // Use the last index that affected the jobs table 700 index, err := state.Index("jobs") 701 if err != nil { 702 return err 703 } 704 reply.Index = index 705 706 // Set the query response 707 j.srv.setQueryMeta(&reply.QueryMeta) 708 return nil 709 }} 710 return j.srv.blockingRPC(&opts) 711 } 712 713 // Allocations is used to list the allocations for a job 714 func (j *Job) Allocations(args *structs.JobSpecificRequest, 715 reply *structs.JobAllocationsResponse) error { 716 if done, err := j.srv.forward("Job.Allocations", args, args, reply); done { 717 return err 718 } 719 defer metrics.MeasureSince([]string{"nomad", "job", "allocations"}, time.Now()) 720 721 // Setup the blocking query 722 opts := blockingOptions{ 723 queryOpts: &args.QueryOptions, 724 queryMeta: &reply.QueryMeta, 725 run: func(ws memdb.WatchSet, state *state.StateStore) error { 726 // Capture the allocations 727 allocs, err := state.AllocsByJob(ws, args.JobID, args.AllAllocs) 728 if err != nil { 729 return err 730 } 731 732 // Convert to stubs 733 if len(allocs) > 0 { 734 reply.Allocations = make([]*structs.AllocListStub, 0, len(allocs)) 735 for _, alloc := range allocs { 736 reply.Allocations = append(reply.Allocations, alloc.Stub()) 737 } 738 } 739 740 // Use the last index that affected the allocs table 741 index, err := state.Index("allocs") 742 if err != nil { 743 return err 744 } 745 reply.Index = index 746 747 // Set the query response 748 j.srv.setQueryMeta(&reply.QueryMeta) 749 return nil 750 751 }} 752 return j.srv.blockingRPC(&opts) 753 } 754 755 // Evaluations is used to list the evaluations for a job 756 func (j *Job) Evaluations(args *structs.JobSpecificRequest, 757 reply *structs.JobEvaluationsResponse) error { 758 if done, err := j.srv.forward("Job.Evaluations", args, args, reply); done { 759 return err 760 } 761 defer metrics.MeasureSince([]string{"nomad", "job", "evaluations"}, time.Now()) 762 763 // Setup the blocking query 764 opts := blockingOptions{ 765 queryOpts: &args.QueryOptions, 766 queryMeta: &reply.QueryMeta, 767 run: func(ws memdb.WatchSet, state *state.StateStore) error { 768 // Capture the evals 769 var err error 770 reply.Evaluations, err = state.EvalsByJob(ws, args.JobID) 771 if err != nil { 772 return err 773 } 774 775 // Use the last index that affected the evals table 776 index, err := state.Index("evals") 777 if err != nil { 778 return err 779 } 780 reply.Index = index 781 782 // Set the query response 783 j.srv.setQueryMeta(&reply.QueryMeta) 784 return nil 785 }} 786 787 return j.srv.blockingRPC(&opts) 788 } 789 790 // Deployments is used to list the deployments for a job 791 func (j *Job) Deployments(args *structs.JobSpecificRequest, 792 reply *structs.DeploymentListResponse) error { 793 if done, err := j.srv.forward("Job.Deployments", args, args, reply); done { 794 return err 795 } 796 defer metrics.MeasureSince([]string{"nomad", "job", "deployments"}, time.Now()) 797 798 // Setup the blocking query 799 opts := blockingOptions{ 800 queryOpts: &args.QueryOptions, 801 queryMeta: &reply.QueryMeta, 802 run: func(ws memdb.WatchSet, state *state.StateStore) error { 803 // Capture the deployments 804 deploys, err := state.DeploymentsByJobID(ws, args.JobID) 805 if err != nil { 806 return err 807 } 808 809 // Use the last index that affected the deployment table 810 index, err := state.Index("deployment") 811 if err != nil { 812 return err 813 } 814 reply.Index = index 815 reply.Deployments = deploys 816 817 // Set the query response 818 j.srv.setQueryMeta(&reply.QueryMeta) 819 return nil 820 821 }} 822 return j.srv.blockingRPC(&opts) 823 } 824 825 // LatestDeployment is used to retrieve the latest deployment for a job 826 func (j *Job) LatestDeployment(args *structs.JobSpecificRequest, 827 reply *structs.SingleDeploymentResponse) error { 828 if done, err := j.srv.forward("Job.LatestDeployment", args, args, reply); done { 829 return err 830 } 831 defer metrics.MeasureSince([]string{"nomad", "job", "latest_deployment"}, time.Now()) 832 833 // Setup the blocking query 834 opts := blockingOptions{ 835 queryOpts: &args.QueryOptions, 836 queryMeta: &reply.QueryMeta, 837 run: func(ws memdb.WatchSet, state *state.StateStore) error { 838 // Capture the deployments 839 deploys, err := state.DeploymentsByJobID(ws, args.JobID) 840 if err != nil { 841 return err 842 } 843 844 // Use the last index that affected the deployment table 845 index, err := state.Index("deployment") 846 if err != nil { 847 return err 848 } 849 reply.Index = index 850 if len(deploys) > 0 { 851 sort.Slice(deploys, func(i, j int) bool { 852 return deploys[i].CreateIndex > deploys[j].CreateIndex 853 }) 854 reply.Deployment = deploys[0] 855 } 856 857 // Set the query response 858 j.srv.setQueryMeta(&reply.QueryMeta) 859 return nil 860 861 }} 862 return j.srv.blockingRPC(&opts) 863 } 864 865 // Plan is used to cause a dry-run evaluation of the Job and return the results 866 // with a potential diff containing annotations. 867 func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse) error { 868 if done, err := j.srv.forward("Job.Plan", args, args, reply); done { 869 return err 870 } 871 defer metrics.MeasureSince([]string{"nomad", "job", "plan"}, time.Now()) 872 873 // Validate the arguments 874 if args.Job == nil { 875 return fmt.Errorf("Job required for plan") 876 } 877 878 // Initialize the job fields (sets defaults and any necessary init work). 879 canonicalizeWarnings := args.Job.Canonicalize() 880 881 // Add implicit constraints 882 setImplicitConstraints(args.Job) 883 884 // Validate the job and capture any warnings 885 err, warnings := validateJob(args.Job) 886 if err != nil { 887 return err 888 } 889 890 // Set the warning message 891 reply.Warnings = structs.MergeMultierrorWarnings(warnings, canonicalizeWarnings) 892 893 // Acquire a snapshot of the state 894 snap, err := j.srv.fsm.State().Snapshot() 895 if err != nil { 896 return err 897 } 898 899 // Get the original job 900 ws := memdb.NewWatchSet() 901 oldJob, err := snap.JobByID(ws, args.Job.ID) 902 if err != nil { 903 return err 904 } 905 906 var index uint64 907 var updatedIndex uint64 908 909 if oldJob != nil { 910 index = oldJob.JobModifyIndex 911 912 // We want to reuse deployments where possible, so only insert the job if 913 // it has changed or the job didn't exist 914 if oldJob.SpecChanged(args.Job) { 915 // Insert the updated Job into the snapshot 916 updatedIndex = oldJob.JobModifyIndex + 1 917 snap.UpsertJob(updatedIndex, args.Job) 918 } 919 } else if oldJob == nil { 920 // Insert the updated Job into the snapshot 921 snap.UpsertJob(100, args.Job) 922 } 923 924 // Create an eval and mark it as requiring annotations and insert that as well 925 eval := &structs.Evaluation{ 926 ID: structs.GenerateUUID(), 927 Priority: args.Job.Priority, 928 Type: args.Job.Type, 929 TriggeredBy: structs.EvalTriggerJobRegister, 930 JobID: args.Job.ID, 931 JobModifyIndex: updatedIndex, 932 Status: structs.EvalStatusPending, 933 AnnotatePlan: true, 934 } 935 936 // Create an in-memory Planner that returns no errors and stores the 937 // submitted plan and created evals. 938 planner := &scheduler.Harness{ 939 State: &snap.StateStore, 940 } 941 942 // Create the scheduler and run it 943 sched, err := scheduler.NewScheduler(eval.Type, j.srv.logger, snap, planner) 944 if err != nil { 945 return err 946 } 947 948 if err := sched.Process(eval); err != nil { 949 return err 950 } 951 952 // Annotate and store the diff 953 if plans := len(planner.Plans); plans != 1 { 954 return fmt.Errorf("scheduler resulted in an unexpected number of plans: %v", plans) 955 } 956 annotations := planner.Plans[0].Annotations 957 if args.Diff { 958 jobDiff, err := oldJob.Diff(args.Job, true) 959 if err != nil { 960 return fmt.Errorf("failed to create job diff: %v", err) 961 } 962 963 if err := scheduler.Annotate(jobDiff, annotations); err != nil { 964 return fmt.Errorf("failed to annotate job diff: %v", err) 965 } 966 reply.Diff = jobDiff 967 } 968 969 // Grab the failures 970 if len(planner.Evals) != 1 { 971 return fmt.Errorf("scheduler resulted in an unexpected number of eval updates: %v", planner.Evals) 972 } 973 updatedEval := planner.Evals[0] 974 975 // If it is a periodic job calculate the next launch 976 if args.Job.IsPeriodic() && args.Job.Periodic.Enabled { 977 reply.NextPeriodicLaunch = args.Job.Periodic.Next(time.Now().In(args.Job.Periodic.GetLocation())) 978 } 979 980 reply.FailedTGAllocs = updatedEval.FailedTGAllocs 981 reply.JobModifyIndex = index 982 reply.Annotations = annotations 983 reply.CreatedEvals = planner.CreateEvals 984 reply.Index = index 985 return nil 986 } 987 988 // validateJob validates a Job and task drivers and returns an error if there is 989 // a validation problem or if the Job is of a type a user is not allowed to 990 // submit. 991 func validateJob(job *structs.Job) (invalid, warnings error) { 992 validationErrors := new(multierror.Error) 993 if err := job.Validate(); err != nil { 994 multierror.Append(validationErrors, err) 995 } 996 997 // Get any warnings 998 warnings = job.Warnings() 999 1000 // Get the signals required 1001 signals := job.RequiredSignals() 1002 1003 // Validate the driver configurations. 1004 for _, tg := range job.TaskGroups { 1005 // Get the signals for the task group 1006 tgSignals, tgOk := signals[tg.Name] 1007 1008 for _, task := range tg.Tasks { 1009 d, err := driver.NewDriver( 1010 task.Driver, 1011 driver.NewEmptyDriverContext(), 1012 ) 1013 if err != nil { 1014 msg := "failed to create driver for task %q in group %q for validation: %v" 1015 multierror.Append(validationErrors, fmt.Errorf(msg, tg.Name, task.Name, err)) 1016 continue 1017 } 1018 1019 if err := d.Validate(task.Config); err != nil { 1020 formatted := fmt.Errorf("group %q -> task %q -> config: %v", tg.Name, task.Name, err) 1021 multierror.Append(validationErrors, formatted) 1022 } 1023 1024 // The task group didn't have any task that required signals 1025 if !tgOk { 1026 continue 1027 } 1028 1029 // This task requires signals. Ensure the driver is capable 1030 if required, ok := tgSignals[task.Name]; ok { 1031 abilities := d.Abilities() 1032 if !abilities.SendSignals { 1033 formatted := fmt.Errorf("group %q -> task %q: driver %q doesn't support sending signals. Requested signals are %v", 1034 tg.Name, task.Name, task.Driver, strings.Join(required, ", ")) 1035 multierror.Append(validationErrors, formatted) 1036 } 1037 } 1038 } 1039 } 1040 1041 if job.Type == structs.JobTypeCore { 1042 multierror.Append(validationErrors, fmt.Errorf("job type cannot be core")) 1043 } 1044 1045 if len(job.Payload) != 0 { 1046 multierror.Append(validationErrors, fmt.Errorf("job can't be submitted with a payload, only dispatched")) 1047 } 1048 1049 return validationErrors.ErrorOrNil(), warnings 1050 } 1051 1052 // validateJobUpdate ensures updates to a job are valid. 1053 func validateJobUpdate(old, new *structs.Job) error { 1054 // Type transitions are disallowed 1055 if old.Type != new.Type { 1056 return fmt.Errorf("cannot update job from type %q to %q", old.Type, new.Type) 1057 } 1058 1059 // Transitioning to/from periodic is disallowed 1060 if old.IsPeriodic() && !new.IsPeriodic() { 1061 return fmt.Errorf("cannot update non-periodic job to being periodic") 1062 } 1063 if new.IsPeriodic() && !old.IsPeriodic() { 1064 return fmt.Errorf("cannot update periodic job to being non-periodic") 1065 } 1066 1067 // Transitioning to/from parameterized is disallowed 1068 if old.IsParameterized() && !new.IsParameterized() { 1069 return fmt.Errorf("cannot update non-parameterized job to being parameterized") 1070 } 1071 if new.IsParameterized() && !old.IsParameterized() { 1072 return fmt.Errorf("cannot update parameterized job to being non-parameterized") 1073 } 1074 1075 return nil 1076 } 1077 1078 // Dispatch a parameterized job. 1079 func (j *Job) Dispatch(args *structs.JobDispatchRequest, reply *structs.JobDispatchResponse) error { 1080 if done, err := j.srv.forward("Job.Dispatch", args, args, reply); done { 1081 return err 1082 } 1083 defer metrics.MeasureSince([]string{"nomad", "job", "dispatch"}, time.Now()) 1084 1085 // Lookup the parameterized job 1086 if args.JobID == "" { 1087 return fmt.Errorf("missing parameterized job ID") 1088 } 1089 1090 snap, err := j.srv.fsm.State().Snapshot() 1091 if err != nil { 1092 return err 1093 } 1094 ws := memdb.NewWatchSet() 1095 parameterizedJob, err := snap.JobByID(ws, args.JobID) 1096 if err != nil { 1097 return err 1098 } 1099 if parameterizedJob == nil { 1100 return fmt.Errorf("parameterized job not found") 1101 } 1102 1103 if !parameterizedJob.IsParameterized() { 1104 return fmt.Errorf("Specified job %q is not a parameterized job", args.JobID) 1105 } 1106 1107 if parameterizedJob.Stop { 1108 return fmt.Errorf("Specified job %q is stopped", args.JobID) 1109 } 1110 1111 // Validate the arguments 1112 if err := validateDispatchRequest(args, parameterizedJob); err != nil { 1113 return err 1114 } 1115 1116 // Derive the child job and commit it via Raft 1117 dispatchJob := parameterizedJob.Copy() 1118 dispatchJob.ParameterizedJob = nil 1119 dispatchJob.ID = structs.DispatchedID(parameterizedJob.ID, time.Now()) 1120 dispatchJob.ParentID = parameterizedJob.ID 1121 dispatchJob.Name = dispatchJob.ID 1122 dispatchJob.SetSubmitTime() 1123 1124 // Merge in the meta data 1125 for k, v := range args.Meta { 1126 if dispatchJob.Meta == nil { 1127 dispatchJob.Meta = make(map[string]string, len(args.Meta)) 1128 } 1129 dispatchJob.Meta[k] = v 1130 } 1131 1132 // Compress the payload 1133 dispatchJob.Payload = snappy.Encode(nil, args.Payload) 1134 1135 regReq := &structs.JobRegisterRequest{ 1136 Job: dispatchJob, 1137 WriteRequest: args.WriteRequest, 1138 } 1139 1140 // Commit this update via Raft 1141 _, jobCreateIndex, err := j.srv.raftApply(structs.JobRegisterRequestType, regReq) 1142 if err != nil { 1143 j.srv.logger.Printf("[ERR] nomad.job: Dispatched job register failed: %v", err) 1144 return err 1145 } 1146 1147 reply.JobCreateIndex = jobCreateIndex 1148 reply.DispatchedJobID = dispatchJob.ID 1149 reply.Index = jobCreateIndex 1150 1151 // If the job is periodic, we don't create an eval. 1152 if !dispatchJob.IsPeriodic() { 1153 // Create a new evaluation 1154 eval := &structs.Evaluation{ 1155 ID: structs.GenerateUUID(), 1156 Priority: dispatchJob.Priority, 1157 Type: dispatchJob.Type, 1158 TriggeredBy: structs.EvalTriggerJobRegister, 1159 JobID: dispatchJob.ID, 1160 JobModifyIndex: jobCreateIndex, 1161 Status: structs.EvalStatusPending, 1162 } 1163 update := &structs.EvalUpdateRequest{ 1164 Evals: []*structs.Evaluation{eval}, 1165 WriteRequest: structs.WriteRequest{Region: args.Region}, 1166 } 1167 1168 // Commit this evaluation via Raft 1169 _, evalIndex, err := j.srv.raftApply(structs.EvalUpdateRequestType, update) 1170 if err != nil { 1171 j.srv.logger.Printf("[ERR] nomad.job: Eval create failed: %v", err) 1172 return err 1173 } 1174 1175 // Setup the reply 1176 reply.EvalID = eval.ID 1177 reply.EvalCreateIndex = evalIndex 1178 reply.Index = evalIndex 1179 } 1180 1181 return nil 1182 } 1183 1184 // validateDispatchRequest returns whether the request is valid given the 1185 // parameterized job. 1186 func validateDispatchRequest(req *structs.JobDispatchRequest, job *structs.Job) error { 1187 // Check the payload constraint is met 1188 hasInputData := len(req.Payload) != 0 1189 if job.ParameterizedJob.Payload == structs.DispatchPayloadRequired && !hasInputData { 1190 return fmt.Errorf("Payload is not provided but required by parameterized job") 1191 } else if job.ParameterizedJob.Payload == structs.DispatchPayloadForbidden && hasInputData { 1192 return fmt.Errorf("Payload provided but forbidden by parameterized job") 1193 } 1194 1195 // Check the payload doesn't exceed the size limit 1196 if l := len(req.Payload); l > DispatchPayloadSizeLimit { 1197 return fmt.Errorf("Payload exceeds maximum size; %d > %d", l, DispatchPayloadSizeLimit) 1198 } 1199 1200 // Check if the metadata is a set 1201 keys := make(map[string]struct{}, len(req.Meta)) 1202 for k := range keys { 1203 if _, ok := keys[k]; ok { 1204 return fmt.Errorf("Duplicate key %q in passed metadata", k) 1205 } 1206 keys[k] = struct{}{} 1207 } 1208 1209 required := helper.SliceStringToSet(job.ParameterizedJob.MetaRequired) 1210 optional := helper.SliceStringToSet(job.ParameterizedJob.MetaOptional) 1211 1212 // Check the metadata key constraints are met 1213 unpermitted := make(map[string]struct{}) 1214 for k := range req.Meta { 1215 _, req := required[k] 1216 _, opt := optional[k] 1217 if !req && !opt { 1218 unpermitted[k] = struct{}{} 1219 } 1220 } 1221 1222 if len(unpermitted) != 0 { 1223 flat := make([]string, 0, len(unpermitted)) 1224 for k := range unpermitted { 1225 flat = append(flat, k) 1226 } 1227 1228 return fmt.Errorf("Dispatch request included unpermitted metadata keys: %v", flat) 1229 } 1230 1231 missing := make(map[string]struct{}) 1232 for _, k := range job.ParameterizedJob.MetaRequired { 1233 if _, ok := req.Meta[k]; !ok { 1234 missing[k] = struct{}{} 1235 } 1236 } 1237 1238 if len(missing) != 0 { 1239 flat := make([]string, 0, len(missing)) 1240 for k := range missing { 1241 flat = append(flat, k) 1242 } 1243 1244 return fmt.Errorf("Dispatch did not provide required meta keys: %v", flat) 1245 } 1246 1247 return nil 1248 }