github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/jobspec/parse_task.go (about) 1 package jobspec 2 3 import ( 4 "fmt" 5 "strings" 6 "time" 7 8 multierror "github.com/hashicorp/go-multierror" 9 "github.com/hashicorp/hcl" 10 "github.com/hashicorp/hcl/hcl/ast" 11 "github.com/hashicorp/nomad/api" 12 "github.com/hashicorp/nomad/helper/pointer" 13 "github.com/mitchellh/mapstructure" 14 ) 15 16 var ( 17 commonTaskKeys = []string{ 18 "driver", 19 "user", 20 "config", 21 "env", 22 "resources", 23 "meta", 24 "logs", 25 "kill_timeout", 26 "shutdown_delay", 27 "kill_signal", 28 "scaling", 29 } 30 31 normalTaskKeys = append(commonTaskKeys, 32 "artifact", 33 "constraint", 34 "affinity", 35 "dispatch_payload", 36 "lifecycle", 37 "leader", 38 "restart", 39 "service", 40 "template", 41 "vault", 42 "kind", 43 "volume_mount", 44 "csi_plugin", 45 ) 46 47 sidecarTaskKeys = append(commonTaskKeys, 48 "name", 49 ) 50 ) 51 52 func parseTasks(result *[]*api.Task, list *ast.ObjectList) error { 53 list = list.Children() 54 if len(list.Items) == 0 { 55 return nil 56 } 57 58 // Go through each object and turn it into an actual result. 59 seen := make(map[string]struct{}) 60 for _, item := range list.Items { 61 n := item.Keys[0].Token.Value().(string) 62 63 // Make sure we haven't already found this 64 if _, ok := seen[n]; ok { 65 return fmt.Errorf("task '%s' defined more than once", n) 66 } 67 seen[n] = struct{}{} 68 69 t, err := parseTask(item, normalTaskKeys) 70 if err != nil { 71 return multierror.Prefix(err, fmt.Sprintf("'%s',", n)) 72 } 73 74 t.Name = n 75 76 *result = append(*result, t) 77 } 78 79 return nil 80 } 81 82 func parseTask(item *ast.ObjectItem, keys []string) (*api.Task, error) { 83 // We need this later 84 var listVal *ast.ObjectList 85 if ot, ok := item.Val.(*ast.ObjectType); ok { 86 listVal = ot.List 87 } else { 88 return nil, fmt.Errorf("should be an object") 89 } 90 91 // Check for invalid keys 92 if err := checkHCLKeys(listVal, keys); err != nil { 93 return nil, err 94 } 95 96 var m map[string]interface{} 97 if err := hcl.DecodeObject(&m, item.Val); err != nil { 98 return nil, err 99 } 100 delete(m, "artifact") 101 delete(m, "config") 102 delete(m, "constraint") 103 delete(m, "affinity") 104 delete(m, "dispatch_payload") 105 delete(m, "lifecycle") 106 delete(m, "env") 107 delete(m, "logs") 108 delete(m, "meta") 109 delete(m, "resources") 110 delete(m, "restart") 111 delete(m, "service") 112 delete(m, "template") 113 delete(m, "vault") 114 delete(m, "volume_mount") 115 delete(m, "csi_plugin") 116 delete(m, "scaling") 117 118 // Build the task 119 var t api.Task 120 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 121 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 122 WeaklyTypedInput: true, 123 Result: &t, 124 }) 125 126 if err != nil { 127 return nil, err 128 } 129 if err := dec.Decode(m); err != nil { 130 return nil, err 131 } 132 133 // If we have env, then parse them 134 if o := listVal.Filter("env"); len(o.Items) > 0 { 135 for _, o := range o.Elem().Items { 136 var m map[string]interface{} 137 if err := hcl.DecodeObject(&m, o.Val); err != nil { 138 return nil, err 139 } 140 if err := mapstructure.WeakDecode(m, &t.Env); err != nil { 141 return nil, err 142 } 143 } 144 } 145 146 if o := listVal.Filter("service"); len(o.Items) > 0 { 147 services, err := parseServices(o) 148 if err != nil { 149 return nil, err 150 } 151 152 t.Services = services 153 } 154 155 if o := listVal.Filter("csi_plugin"); len(o.Items) > 0 { 156 if len(o.Items) != 1 { 157 return nil, fmt.Errorf("csi_plugin -> Expected single stanza, got %d", len(o.Items)) 158 } 159 i := o.Elem().Items[0] 160 161 var m map[string]interface{} 162 var cfg api.TaskCSIPluginConfig 163 if err := hcl.DecodeObject(&m, i.Val); err != nil { 164 return nil, err 165 } 166 167 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 168 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 169 WeaklyTypedInput: true, 170 Result: &cfg, 171 }) 172 if err != nil { 173 return nil, err 174 } 175 if err := dec.Decode(m); err != nil { 176 return nil, err 177 } 178 179 t.CSIPluginConfig = &cfg 180 } 181 182 // If we have config, then parse that 183 if o := listVal.Filter("config"); len(o.Items) > 0 { 184 for _, o := range o.Elem().Items { 185 var m map[string]interface{} 186 if err := hcl.DecodeObject(&m, o.Val); err != nil { 187 return nil, err 188 } 189 190 if err := mapstructure.WeakDecode(m, &t.Config); err != nil { 191 return nil, err 192 } 193 } 194 } 195 196 // Parse constraints 197 if o := listVal.Filter("constraint"); len(o.Items) > 0 { 198 if err := parseConstraints(&t.Constraints, o); err != nil { 199 return nil, multierror.Prefix(err, "constraint ->") 200 } 201 } 202 203 // Parse affinities 204 if o := listVal.Filter("affinity"); len(o.Items) > 0 { 205 if err := parseAffinities(&t.Affinities, o); err != nil { 206 return nil, multierror.Prefix(err, "affinity ->") 207 } 208 } 209 210 // Parse out meta fields. These are in HCL as a list so we need 211 // to iterate over them and merge them. 212 if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 { 213 for _, o := range metaO.Elem().Items { 214 var m map[string]interface{} 215 if err := hcl.DecodeObject(&m, o.Val); err != nil { 216 return nil, err 217 } 218 if err := mapstructure.WeakDecode(m, &t.Meta); err != nil { 219 return nil, err 220 } 221 } 222 } 223 224 // Parse volume mounts 225 if o := listVal.Filter("volume_mount"); len(o.Items) > 0 { 226 if err := parseVolumeMounts(&t.VolumeMounts, o); err != nil { 227 return nil, multierror.Prefix(err, "volume_mount ->") 228 } 229 } 230 231 // If we have resources, then parse that 232 if o := listVal.Filter("resources"); len(o.Items) > 0 { 233 var r api.Resources 234 if err := parseResources(&r, o); err != nil { 235 return nil, multierror.Prefix(err, "resources ->") 236 } 237 238 t.Resources = &r 239 } 240 241 // Parse restart policy 242 if o := listVal.Filter("restart"); len(o.Items) > 0 { 243 if err := parseRestartPolicy(&t.RestartPolicy, o); err != nil { 244 return nil, multierror.Prefix(err, "restart ->") 245 } 246 } 247 248 // If we have logs then parse that 249 if o := listVal.Filter("logs"); len(o.Items) > 0 { 250 if len(o.Items) > 1 { 251 return nil, fmt.Errorf("only one logs block is allowed in a Task. Number of logs block found: %d", len(o.Items)) 252 } 253 var m map[string]interface{} 254 logsBlock := o.Items[0] 255 256 // Check for invalid keys 257 valid := []string{ 258 "max_files", 259 "max_file_size", 260 } 261 if err := checkHCLKeys(logsBlock.Val, valid); err != nil { 262 return nil, multierror.Prefix(err, "logs ->") 263 } 264 265 if err := hcl.DecodeObject(&m, logsBlock.Val); err != nil { 266 return nil, err 267 } 268 269 var log api.LogConfig 270 if err := mapstructure.WeakDecode(m, &log); err != nil { 271 return nil, err 272 } 273 274 t.LogConfig = &log 275 } 276 277 // Parse artifacts 278 if o := listVal.Filter("artifact"); len(o.Items) > 0 { 279 if err := parseArtifacts(&t.Artifacts, o); err != nil { 280 return nil, multierror.Prefix(err, "artifact ->") 281 } 282 } 283 284 // Parse templates 285 if o := listVal.Filter("template"); len(o.Items) > 0 { 286 if err := parseTemplates(&t.Templates, o); err != nil { 287 return nil, multierror.Prefix(err, "template ->") 288 } 289 } 290 291 // Parse scaling policies 292 if o := listVal.Filter("scaling"); len(o.Items) > 0 { 293 if err := parseTaskScalingPolicies(&t.ScalingPolicies, o); err != nil { 294 return nil, err 295 } 296 } 297 298 // If we have a vault block, then parse that 299 if o := listVal.Filter("vault"); len(o.Items) > 0 { 300 v := &api.Vault{ 301 Env: boolToPtr(true), 302 ChangeMode: stringToPtr("restart"), 303 } 304 305 if err := parseVault(v, o); err != nil { 306 return nil, multierror.Prefix(err, "vault ->") 307 } 308 309 t.Vault = v 310 } 311 312 // If we have a dispatch_payload block parse that 313 if o := listVal.Filter("dispatch_payload"); len(o.Items) > 0 { 314 if len(o.Items) > 1 { 315 return nil, fmt.Errorf("only one dispatch_payload block is allowed in a task. Number of dispatch_payload blocks found: %d", len(o.Items)) 316 } 317 var m map[string]interface{} 318 dispatchBlock := o.Items[0] 319 320 // Check for invalid keys 321 valid := []string{ 322 "file", 323 } 324 if err := checkHCLKeys(dispatchBlock.Val, valid); err != nil { 325 return nil, multierror.Prefix(err, "dispatch_payload ->") 326 } 327 328 if err := hcl.DecodeObject(&m, dispatchBlock.Val); err != nil { 329 return nil, err 330 } 331 332 t.DispatchPayload = &api.DispatchPayloadConfig{} 333 if err := mapstructure.WeakDecode(m, t.DispatchPayload); err != nil { 334 return nil, err 335 } 336 } 337 338 // If we have a lifecycle block parse that 339 if o := listVal.Filter("lifecycle"); len(o.Items) > 0 { 340 if len(o.Items) > 1 { 341 return nil, fmt.Errorf("only one lifecycle block is allowed in a task. Number of lifecycle blocks found: %d", len(o.Items)) 342 } 343 344 var m map[string]interface{} 345 lifecycleBlock := o.Items[0] 346 347 // Check for invalid keys 348 valid := []string{ 349 "hook", 350 "sidecar", 351 } 352 if err := checkHCLKeys(lifecycleBlock.Val, valid); err != nil { 353 return nil, multierror.Prefix(err, "lifecycle ->") 354 } 355 356 if err := hcl.DecodeObject(&m, lifecycleBlock.Val); err != nil { 357 return nil, err 358 } 359 360 t.Lifecycle = &api.TaskLifecycle{} 361 if err := mapstructure.WeakDecode(m, t.Lifecycle); err != nil { 362 return nil, err 363 } 364 } 365 return &t, nil 366 } 367 368 func parseArtifacts(result *[]*api.TaskArtifact, list *ast.ObjectList) error { 369 for _, o := range list.Elem().Items { 370 // Check for invalid keys 371 valid := []string{ 372 "source", 373 "options", 374 "headers", 375 "mode", 376 "destination", 377 } 378 if err := checkHCLKeys(o.Val, valid); err != nil { 379 return err 380 } 381 382 var m map[string]interface{} 383 if err := hcl.DecodeObject(&m, o.Val); err != nil { 384 return err 385 } 386 387 delete(m, "options") 388 389 var ta api.TaskArtifact 390 if err := mapstructure.WeakDecode(m, &ta); err != nil { 391 return err 392 } 393 394 var optionList *ast.ObjectList 395 if ot, ok := o.Val.(*ast.ObjectType); ok { 396 optionList = ot.List 397 } else { 398 return fmt.Errorf("artifact should be an object") 399 } 400 401 if oo := optionList.Filter("options"); len(oo.Items) > 0 { 402 options := make(map[string]string) 403 if err := parseArtifactOption(options, oo); err != nil { 404 return multierror.Prefix(err, "options: ") 405 } 406 ta.GetterOptions = options 407 } 408 409 *result = append(*result, &ta) 410 } 411 412 return nil 413 } 414 415 func parseArtifactOption(result map[string]string, list *ast.ObjectList) error { 416 list = list.Elem() 417 if len(list.Items) > 1 { 418 return fmt.Errorf("only one 'options' block allowed per artifact") 419 } 420 421 // Get our resource object 422 o := list.Items[0] 423 424 var m map[string]interface{} 425 if err := hcl.DecodeObject(&m, o.Val); err != nil { 426 return err 427 } 428 429 if err := mapstructure.WeakDecode(m, &result); err != nil { 430 return err 431 } 432 433 return nil 434 } 435 436 func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error { 437 for _, o := range list.Elem().Items { 438 // we'll need a list of all ast objects for later 439 var listVal *ast.ObjectList 440 if ot, ok := o.Val.(*ast.ObjectType); ok { 441 listVal = ot.List 442 } else { 443 return fmt.Errorf("should be an object") 444 } 445 446 // Check for invalid keys 447 valid := []string{ 448 "change_mode", 449 "change_signal", 450 "change_script", 451 "data", 452 "destination", 453 "left_delimiter", 454 "perms", 455 "uid", 456 "gid", 457 "right_delimiter", 458 "source", 459 "splay", 460 "env", 461 "vault_grace", //COMPAT(0.12) not used; emits warning in 0.11. 462 "wait", 463 "error_on_missing_key", 464 } 465 if err := checkHCLKeys(o.Val, valid); err != nil { 466 return err 467 } 468 469 var m map[string]interface{} 470 if err := hcl.DecodeObject(&m, o.Val); err != nil { 471 return err 472 } 473 delete(m, "change_script") // change_script is its own object 474 475 templ := &api.Template{ 476 ChangeMode: stringToPtr("restart"), 477 Splay: timeToPtr(5 * time.Second), 478 Perms: stringToPtr("0644"), 479 Uid: pointer.Of(-1), 480 Gid: pointer.Of(-1), 481 ErrMissingKey: pointer.Of(false), 482 } 483 484 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 485 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 486 WeaklyTypedInput: true, 487 Result: templ, 488 }) 489 if err != nil { 490 return err 491 } 492 if err := dec.Decode(m); err != nil { 493 return err 494 } 495 496 // If we have change_script, parse it 497 if o := listVal.Filter("change_script"); len(o.Items) > 0 { 498 if len(o.Items) != 1 { 499 return fmt.Errorf( 500 "change_script -> expected single stanza, got %d", len(o.Items), 501 ) 502 } 503 var m map[string]interface{} 504 changeScriptBlock := o.Items[0] 505 506 // check for invalid fields 507 valid := []string{"command", "args", "timeout", "fail_on_error"} 508 if err := checkHCLKeys(changeScriptBlock.Val, valid); err != nil { 509 return multierror.Prefix(err, "change_script ->") 510 } 511 512 if err := hcl.DecodeObject(&m, changeScriptBlock.Val); err != nil { 513 return err 514 } 515 516 templ.ChangeScript = &api.ChangeScript{} 517 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 518 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 519 WeaklyTypedInput: true, 520 Result: templ.ChangeScript, 521 }) 522 if err != nil { 523 return err 524 } 525 if err := dec.Decode(m); err != nil { 526 return err 527 } 528 } 529 530 *result = append(*result, templ) 531 } 532 533 return nil 534 } 535 536 func parseTaskScalingPolicies(result *[]*api.ScalingPolicy, list *ast.ObjectList) error { 537 if len(list.Items) == 0 { 538 return nil 539 } 540 541 errPrefix := "scaling ->" 542 // Go through each object and turn it into an actual result. 543 seen := make(map[string]bool) 544 for _, item := range list.Items { 545 if l := len(item.Keys); l == 0 { 546 return multierror.Prefix(fmt.Errorf("task scaling policy missing name"), errPrefix) 547 } else if l > 1 { 548 return multierror.Prefix(fmt.Errorf("task scaling policy should only have one name"), errPrefix) 549 } 550 n := item.Keys[0].Token.Value().(string) 551 errPrefix = fmt.Sprintf("scaling[%v] ->", n) 552 553 var policyType string 554 switch strings.ToLower(n) { 555 case "cpu": 556 policyType = "vertical_cpu" 557 case "mem": 558 policyType = "vertical_mem" 559 default: 560 return multierror.Prefix(fmt.Errorf(`scaling policy name must be "cpu" or "mem"`), errPrefix) 561 } 562 563 // Make sure we haven't already found this 564 if seen[n] { 565 return multierror.Prefix(fmt.Errorf("scaling policy cannot be defined more than once"), errPrefix) 566 } 567 seen[n] = true 568 569 p, err := parseScalingPolicy(item) 570 if err != nil { 571 return multierror.Prefix(err, errPrefix) 572 } 573 574 if p.Type == "" { 575 p.Type = policyType 576 } else if p.Type != policyType { 577 return multierror.Prefix(fmt.Errorf("policy had invalid 'type': %q", p.Type), errPrefix) 578 } 579 580 *result = append(*result, p) 581 } 582 583 return nil 584 } 585 586 func parseResources(result *api.Resources, list *ast.ObjectList) error { 587 list = list.Elem() 588 if len(list.Items) == 0 { 589 return nil 590 } 591 if len(list.Items) > 1 { 592 return fmt.Errorf("only one 'resource' block allowed per task") 593 } 594 595 // Get our resource object 596 o := list.Items[0] 597 598 // We need this later 599 var listVal *ast.ObjectList 600 if ot, ok := o.Val.(*ast.ObjectType); ok { 601 listVal = ot.List 602 } else { 603 return fmt.Errorf("resource: should be an object") 604 } 605 606 // Check for invalid keys 607 valid := []string{ 608 "cpu", 609 "iops", // COMPAT(0.10): Remove after one release to allow it to be removed from jobspecs 610 "disk", 611 "memory", 612 "memory_max", 613 "network", 614 "device", 615 "cores", 616 } 617 if err := checkHCLKeys(listVal, valid); err != nil { 618 return multierror.Prefix(err, "resources ->") 619 } 620 621 var m map[string]interface{} 622 if err := hcl.DecodeObject(&m, o.Val); err != nil { 623 return err 624 } 625 delete(m, "network") 626 delete(m, "device") 627 628 if err := mapstructure.WeakDecode(m, result); err != nil { 629 return err 630 } 631 632 // Parse the network resources 633 if o := listVal.Filter("network"); len(o.Items) > 0 { 634 r, err := ParseNetwork(o) 635 if err != nil { 636 return fmt.Errorf("resource, %v", err) 637 } 638 result.Networks = []*api.NetworkResource{r} 639 } 640 641 // Parse the device resources 642 if o := listVal.Filter("device"); len(o.Items) > 0 { 643 result.Devices = make([]*api.RequestedDevice, len(o.Items)) 644 for idx, do := range o.Items { 645 if l := len(do.Keys); l == 0 { 646 return multierror.Prefix(fmt.Errorf("missing device name"), fmt.Sprintf("resources, device[%d]->", idx)) 647 } else if l > 1 { 648 return multierror.Prefix(fmt.Errorf("only one name may be specified"), fmt.Sprintf("resources, device[%d]->", idx)) 649 } 650 name := do.Keys[0].Token.Value().(string) 651 652 // Value should be an object 653 var listVal *ast.ObjectList 654 if ot, ok := do.Val.(*ast.ObjectType); ok { 655 listVal = ot.List 656 } else { 657 return fmt.Errorf("device should be an object") 658 } 659 660 // Check for invalid keys 661 valid := []string{ 662 "name", 663 "count", 664 "affinity", 665 "constraint", 666 } 667 if err := checkHCLKeys(do.Val, valid); err != nil { 668 return multierror.Prefix(err, fmt.Sprintf("resources, device[%d]->", idx)) 669 } 670 671 // Set the name 672 var r api.RequestedDevice 673 r.Name = name 674 675 var m map[string]interface{} 676 if err := hcl.DecodeObject(&m, do.Val); err != nil { 677 return err 678 } 679 680 delete(m, "constraint") 681 delete(m, "affinity") 682 683 if err := mapstructure.WeakDecode(m, &r); err != nil { 684 return err 685 } 686 687 // Parse constraints 688 if o := listVal.Filter("constraint"); len(o.Items) > 0 { 689 if err := parseConstraints(&r.Constraints, o); err != nil { 690 return multierror.Prefix(err, "constraint ->") 691 } 692 } 693 694 // Parse affinities 695 if o := listVal.Filter("affinity"); len(o.Items) > 0 { 696 if err := parseAffinities(&r.Affinities, o); err != nil { 697 return multierror.Prefix(err, "affinity ->") 698 } 699 } 700 701 result.Devices[idx] = &r 702 } 703 } 704 705 return nil 706 } 707 708 func parseVolumeMounts(out *[]*api.VolumeMount, list *ast.ObjectList) error { 709 mounts := make([]*api.VolumeMount, len(list.Items)) 710 711 for i, item := range list.Items { 712 valid := []string{ 713 "volume", 714 "read_only", 715 "destination", 716 "propagation_mode", 717 } 718 if err := checkHCLKeys(item.Val, valid); err != nil { 719 return err 720 } 721 722 var m map[string]interface{} 723 if err := hcl.DecodeObject(&m, item.Val); err != nil { 724 return err 725 } 726 727 var result api.VolumeMount 728 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 729 WeaklyTypedInput: true, 730 Result: &result, 731 }) 732 if err != nil { 733 return err 734 } 735 if err := dec.Decode(m); err != nil { 736 return err 737 } 738 739 mounts[i] = &result 740 } 741 742 *out = mounts 743 return nil 744 }