github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/nomad/job_endpoint.go (about)

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