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