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 }