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