github.com/hernad/nomad@v1.6.112/nomad/job_endpoint_hooks.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package nomad
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/dustin/go-humanize"
    10  	"github.com/hashicorp/go-multierror"
    11  	"github.com/hernad/nomad/helper"
    12  	"github.com/hernad/nomad/nomad/structs"
    13  )
    14  
    15  const (
    16  	attrVaultVersion      = `${attr.vault.version}`
    17  	attrConsulVersion     = `${attr.consul.version}`
    18  	attrNomadVersion      = `${attr.nomad.version}`
    19  	attrNomadServiceDisco = `${attr.nomad.service_discovery}`
    20  )
    21  
    22  var (
    23  	// vaultConstraint is the implicit constraint added to jobs requesting a
    24  	// Vault token
    25  	vaultConstraint = &structs.Constraint{
    26  		LTarget: attrVaultVersion,
    27  		RTarget: ">= 0.6.1",
    28  		Operand: structs.ConstraintSemver,
    29  	}
    30  
    31  	// consulServiceDiscoveryConstraint is the implicit constraint added to
    32  	// task groups which include services utilising the Consul provider. The
    33  	// Consul version is pinned to a minimum of that which introduced the
    34  	// namespace feature.
    35  	consulServiceDiscoveryConstraint = &structs.Constraint{
    36  		LTarget: attrConsulVersion,
    37  		RTarget: ">= 1.7.0",
    38  		Operand: structs.ConstraintSemver,
    39  	}
    40  
    41  	// nativeServiceDiscoveryConstraint is the constraint injected into task
    42  	// groups that utilise Nomad's native service discovery feature. This is
    43  	// needed, as operators can disable the client functionality, and therefore
    44  	// we need to ensure task groups are placed where they can run
    45  	// successfully.
    46  	nativeServiceDiscoveryConstraint = &structs.Constraint{
    47  		LTarget: attrNomadServiceDisco,
    48  		RTarget: "true",
    49  		Operand: "=",
    50  	}
    51  
    52  	// nativeServiceDiscoveryChecksConstraint is the constraint injected into task
    53  	// groups that utilize Nomad's native service discovery checks feature. This
    54  	// is needed, as operators can have versions of Nomad pre-v1.4 mixed into a
    55  	// cluster with v1.4 servers, causing jobs to be placed on incompatible
    56  	// clients.
    57  	nativeServiceDiscoveryChecksConstraint = &structs.Constraint{
    58  		LTarget: attrNomadVersion,
    59  		RTarget: ">= 1.4.0",
    60  		Operand: structs.ConstraintSemver,
    61  	}
    62  )
    63  
    64  type admissionController interface {
    65  	Name() string
    66  }
    67  
    68  type jobMutator interface {
    69  	admissionController
    70  	Mutate(*structs.Job) (out *structs.Job, warnings []error, err error)
    71  }
    72  
    73  type jobValidator interface {
    74  	admissionController
    75  	Validate(*structs.Job) (warnings []error, err error)
    76  }
    77  
    78  func (j *Job) admissionControllers(job *structs.Job) (out *structs.Job, warnings []error, err error) {
    79  	// Mutators run first before validators, so validators view the final rendered job.
    80  	// So, mutators must handle invalid jobs.
    81  	out, warnings, err = j.admissionMutators(job)
    82  	if err != nil {
    83  		return nil, nil, err
    84  	}
    85  
    86  	validateWarnings, err := j.admissionValidators(job)
    87  	if err != nil {
    88  		return nil, nil, err
    89  	}
    90  	warnings = append(warnings, validateWarnings...)
    91  
    92  	return out, warnings, nil
    93  }
    94  
    95  // admissionMutator returns an updated job as well as warnings or an error.
    96  func (j *Job) admissionMutators(job *structs.Job) (_ *structs.Job, warnings []error, err error) {
    97  	var w []error
    98  	for _, mutator := range j.mutators {
    99  		job, w, err = mutator.Mutate(job)
   100  		j.logger.Trace("job mutate results", "mutator", mutator.Name(), "warnings", w, "error", err)
   101  		if err != nil {
   102  			return nil, nil, fmt.Errorf("error in job mutator %s: %v", mutator.Name(), err)
   103  		}
   104  		warnings = append(warnings, w...)
   105  	}
   106  	return job, warnings, err
   107  }
   108  
   109  // admissionValidators returns a slice of validation warnings and a multierror
   110  // of validation failures.
   111  func (j *Job) admissionValidators(origJob *structs.Job) ([]error, error) {
   112  	// ensure job is not mutated
   113  	job := origJob.Copy()
   114  
   115  	var warnings []error
   116  	var errs error
   117  
   118  	for _, validator := range j.validators {
   119  		w, err := validator.Validate(job)
   120  		j.logger.Trace("job validate results", "validator", validator.Name(), "warnings", w, "error", err)
   121  		if err != nil {
   122  			errs = multierror.Append(errs, err)
   123  		}
   124  		warnings = append(warnings, w...)
   125  	}
   126  
   127  	return warnings, errs
   128  
   129  }
   130  
   131  // jobCanonicalizer calls job.Canonicalize (sets defaults and initializes
   132  // fields) and returns any errors as warnings.
   133  type jobCanonicalizer struct {
   134  	srv *Server
   135  }
   136  
   137  func (c *jobCanonicalizer) Name() string {
   138  	return "canonicalize"
   139  }
   140  
   141  func (c *jobCanonicalizer) Mutate(job *structs.Job) (*structs.Job, []error, error) {
   142  	job.Canonicalize()
   143  
   144  	// If the job priority is not set, we fallback on the defaults specified in the server config
   145  	if job.Priority == 0 {
   146  		job.Priority = c.srv.GetConfig().JobDefaultPriority
   147  	}
   148  
   149  	return job, nil, nil
   150  }
   151  
   152  // jobImpliedConstraints adds constraints to a job implied by other job fields
   153  // and blocks.
   154  type jobImpliedConstraints struct{}
   155  
   156  func (jobImpliedConstraints) Name() string {
   157  	return "constraints"
   158  }
   159  
   160  func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, error) {
   161  	// Get the Vault blocks in the job
   162  	vaultBlocks := j.Vault()
   163  
   164  	// Get the required signals
   165  	signals := j.RequiredSignals()
   166  
   167  	// Identify which task groups are utilising Nomad native service discovery.
   168  	nativeServiceDisco := j.RequiredNativeServiceDiscovery()
   169  
   170  	// Identify which task groups are utilising Consul service discovery.
   171  	consulServiceDisco := j.RequiredConsulServiceDiscovery()
   172  
   173  	// Hot path
   174  	if len(signals) == 0 && len(vaultBlocks) == 0 &&
   175  		nativeServiceDisco.Empty() && len(consulServiceDisco) == 0 {
   176  		return j, nil, nil
   177  	}
   178  
   179  	// Iterate through all the task groups within the job and add any required
   180  	// constraints. When adding new implicit constraints, they should go inside
   181  	// this single loop, with a new constraintMatcher if needed.
   182  	for _, tg := range j.TaskGroups {
   183  
   184  		// If the task group utilises Vault, run the mutator.
   185  		if _, ok := vaultBlocks[tg.Name]; ok {
   186  			mutateConstraint(constraintMatcherLeft, tg, vaultConstraint)
   187  		}
   188  
   189  		// Check whether the task group is using signals. In the case that it
   190  		// is, we flatten the signals and build a constraint, then run the
   191  		// mutator.
   192  		if tgSignals, ok := signals[tg.Name]; ok {
   193  			required := helper.UniqueMapSliceValues(tgSignals)
   194  			sigConstraint := getSignalConstraint(required)
   195  			mutateConstraint(constraintMatcherFull, tg, sigConstraint)
   196  		}
   197  
   198  		// If the task group utilises Nomad service discovery, run the mutator.
   199  		if nativeServiceDisco.Basic.Contains(tg.Name) {
   200  			mutateConstraint(constraintMatcherFull, tg, nativeServiceDiscoveryConstraint)
   201  		}
   202  
   203  		// If the task group utilizes NSD checks, run the mutator.
   204  		if nativeServiceDisco.Checks.Contains(tg.Name) {
   205  			mutateConstraint(constraintMatcherFull, tg, nativeServiceDiscoveryChecksConstraint)
   206  		}
   207  
   208  		// If the task group utilises Consul service discovery, run the mutator.
   209  		if ok := consulServiceDisco[tg.Name]; ok {
   210  			mutateConstraint(constraintMatcherLeft, tg, consulServiceDiscoveryConstraint)
   211  		}
   212  	}
   213  
   214  	return j, nil, nil
   215  }
   216  
   217  // constraintMatcher is a custom type which helps control how constraints are
   218  // identified as being present within a task group.
   219  type constraintMatcher uint
   220  
   221  const (
   222  	// constraintMatcherFull ensures that a constraint is only considered found
   223  	// when they match totally. This check is performed using the
   224  	// structs.Constraint Equal function.
   225  	constraintMatcherFull constraintMatcher = iota
   226  
   227  	// constraintMatcherLeft ensure that a constraint is considered found if
   228  	// the constraints LTarget is matched only. This allows an existing
   229  	// constraint to override the proposed implicit one.
   230  	constraintMatcherLeft
   231  )
   232  
   233  // mutateConstraint is a generic mutator used to set implicit constraints
   234  // within the task group if they are needed.
   235  func mutateConstraint(matcher constraintMatcher, taskGroup *structs.TaskGroup, constraint *structs.Constraint) {
   236  
   237  	var found bool
   238  
   239  	// It's possible to switch on the matcher within the constraint loop to
   240  	// reduce repetition. This, however, means switching per constraint,
   241  	// therefore we do it here.
   242  	switch matcher {
   243  	case constraintMatcherFull:
   244  		for _, c := range taskGroup.Constraints {
   245  			if c.Equal(constraint) {
   246  				found = true
   247  				break
   248  			}
   249  		}
   250  	case constraintMatcherLeft:
   251  		for _, c := range taskGroup.Constraints {
   252  			if c.LTarget == constraint.LTarget {
   253  				found = true
   254  				break
   255  			}
   256  		}
   257  	}
   258  
   259  	// If we didn't find a suitable constraint match, add one.
   260  	if !found {
   261  		taskGroup.Constraints = append(taskGroup.Constraints, constraint)
   262  	}
   263  }
   264  
   265  // jobValidate validates a Job and task drivers and returns an error if there is
   266  // a validation problem or if the Job is of a type a user is not allowed to
   267  // submit.
   268  type jobValidate struct {
   269  	srv *Server
   270  }
   271  
   272  func (*jobValidate) Name() string {
   273  	return "validate"
   274  }
   275  
   276  func (v *jobValidate) Validate(job *structs.Job) (warnings []error, err error) {
   277  	validationErrors := new(multierror.Error)
   278  	if err := job.Validate(); err != nil {
   279  		multierror.Append(validationErrors, err)
   280  	}
   281  
   282  	// Get any warnings
   283  	jobWarnings := job.Warnings()
   284  	if jobWarnings != nil {
   285  		if multi, ok := jobWarnings.(*multierror.Error); ok {
   286  			// Unpack multiple warnings
   287  			warnings = append(warnings, multi.Errors...)
   288  		} else {
   289  			warnings = append(warnings, jobWarnings)
   290  		}
   291  	}
   292  
   293  	// TODO: Validate the driver configurations. These had to be removed in 0.9
   294  	//       to support driver plugins, but see issue: #XXXX for more info.
   295  
   296  	if job.Type == structs.JobTypeCore {
   297  		multierror.Append(validationErrors, fmt.Errorf("job type cannot be core"))
   298  	}
   299  
   300  	if len(job.Payload) != 0 {
   301  		multierror.Append(validationErrors, fmt.Errorf("job can't be submitted with a payload, only dispatched"))
   302  	}
   303  
   304  	if job.Priority < structs.JobMinPriority || job.Priority > v.srv.config.JobMaxPriority {
   305  		multierror.Append(validationErrors, fmt.Errorf("job priority must be between [%d, %d]", structs.JobMinPriority, v.srv.config.JobMaxPriority))
   306  	}
   307  
   308  	return warnings, validationErrors.ErrorOrNil()
   309  }
   310  
   311  type memoryOversubscriptionValidate struct {
   312  	srv *Server
   313  }
   314  
   315  func (*memoryOversubscriptionValidate) Name() string {
   316  	return "memory_oversubscription"
   317  }
   318  
   319  func (v *memoryOversubscriptionValidate) Validate(job *structs.Job) (warnings []error, err error) {
   320  	_, c, err := v.srv.State().SchedulerConfig()
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	pool, err := v.srv.State().NodePoolByName(nil, job.NodePool)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	if pool.MemoryOversubscriptionEnabled(c) {
   331  		return nil, nil
   332  	}
   333  
   334  	for _, tg := range job.TaskGroups {
   335  		for _, t := range tg.Tasks {
   336  			if t.Resources != nil && t.Resources.MemoryMaxMB != 0 {
   337  				warnings = append(warnings, fmt.Errorf("Memory oversubscription is not enabled; Task \"%v.%v\" memory_max value will be ignored. Update the Scheduler Configuration to allow oversubscription.", tg.Name, t.Name))
   338  			}
   339  		}
   340  	}
   341  
   342  	return warnings, err
   343  }
   344  
   345  // submissionController is used to protect against job source sizes that exceed
   346  // the maximum as set in server config as job_max_source_size
   347  //
   348  // Such jobs will have their source discarded and emit a warning, but the job
   349  // itself will still continue with being registered.
   350  func (j *Job) submissionController(args *structs.JobRegisterRequest) error {
   351  	if args.Submission == nil {
   352  		return nil
   353  	}
   354  	maxSize := j.srv.GetConfig().JobMaxSourceSize
   355  	submission := args.Submission
   356  	// discard the submission if the source + variables is larger than the maximum
   357  	// allowable size as set by client config
   358  	totalSize := len(submission.Source)
   359  	totalSize += len(submission.Variables)
   360  	for key, value := range submission.VariableFlags {
   361  		totalSize += len(key)
   362  		totalSize += len(value)
   363  	}
   364  	if totalSize > maxSize {
   365  		args.Submission = nil
   366  		totalSizeHuman := humanize.Bytes(uint64(totalSize))
   367  		maxSizeHuman := humanize.Bytes(uint64(maxSize))
   368  		return fmt.Errorf("job source size of %s exceeds maximum of %s and will be discarded", totalSizeHuman, maxSizeHuman)
   369  	}
   370  	return nil
   371  }