github.com/emate/nomad@v0.8.2-wo-binpacking/jobspec/parse.go (about) 1 package jobspec 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "regexp" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/hashicorp/go-multierror" 15 "github.com/hashicorp/hcl" 16 "github.com/hashicorp/hcl/hcl/ast" 17 "github.com/hashicorp/nomad/api" 18 "github.com/hashicorp/nomad/helper" 19 "github.com/hashicorp/nomad/nomad/structs" 20 "github.com/mitchellh/mapstructure" 21 ) 22 23 var reDynamicPorts = regexp.MustCompile("^[a-zA-Z0-9_]+$") 24 var errPortLabel = fmt.Errorf("Port label does not conform to naming requirements %s", reDynamicPorts.String()) 25 26 // Parse parses the job spec from the given io.Reader. 27 // 28 // Due to current internal limitations, the entire contents of the 29 // io.Reader will be copied into memory first before parsing. 30 func Parse(r io.Reader) (*api.Job, error) { 31 // Copy the reader into an in-memory buffer first since HCL requires it. 32 var buf bytes.Buffer 33 if _, err := io.Copy(&buf, r); err != nil { 34 return nil, err 35 } 36 37 // Parse the buffer 38 root, err := hcl.Parse(buf.String()) 39 if err != nil { 40 return nil, fmt.Errorf("error parsing: %s", err) 41 } 42 buf.Reset() 43 44 // Top-level item should be a list 45 list, ok := root.Node.(*ast.ObjectList) 46 if !ok { 47 return nil, fmt.Errorf("error parsing: root should be an object") 48 } 49 50 // Check for invalid keys 51 valid := []string{ 52 "job", 53 } 54 if err := helper.CheckHCLKeys(list, valid); err != nil { 55 return nil, err 56 } 57 58 var job api.Job 59 60 // Parse the job out 61 matches := list.Filter("job") 62 if len(matches.Items) == 0 { 63 return nil, fmt.Errorf("'job' stanza not found") 64 } 65 if err := parseJob(&job, matches); err != nil { 66 return nil, fmt.Errorf("error parsing 'job': %s", err) 67 } 68 69 return &job, nil 70 } 71 72 // ParseFile parses the given path as a job spec. 73 func ParseFile(path string) (*api.Job, error) { 74 path, err := filepath.Abs(path) 75 if err != nil { 76 return nil, err 77 } 78 79 f, err := os.Open(path) 80 if err != nil { 81 return nil, err 82 } 83 defer f.Close() 84 85 return Parse(f) 86 } 87 88 func parseJob(result *api.Job, list *ast.ObjectList) error { 89 if len(list.Items) != 1 { 90 return fmt.Errorf("only one 'job' block allowed") 91 } 92 list = list.Children() 93 if len(list.Items) != 1 { 94 return fmt.Errorf("'job' block missing name") 95 } 96 97 // Get our job object 98 obj := list.Items[0] 99 100 // Decode the full thing into a map[string]interface for ease 101 var m map[string]interface{} 102 if err := hcl.DecodeObject(&m, obj.Val); err != nil { 103 return err 104 } 105 delete(m, "constraint") 106 delete(m, "meta") 107 delete(m, "migrate") 108 delete(m, "parameterized") 109 delete(m, "periodic") 110 delete(m, "reschedule") 111 delete(m, "update") 112 delete(m, "vault") 113 114 // Set the ID and name to the object key 115 result.ID = helper.StringToPtr(obj.Keys[0].Token.Value().(string)) 116 result.Name = helper.StringToPtr(*result.ID) 117 118 // Decode the rest 119 if err := mapstructure.WeakDecode(m, result); err != nil { 120 return err 121 } 122 123 // Value should be an object 124 var listVal *ast.ObjectList 125 if ot, ok := obj.Val.(*ast.ObjectType); ok { 126 listVal = ot.List 127 } else { 128 return fmt.Errorf("job '%s' value: should be an object", *result.ID) 129 } 130 131 // Check for invalid keys 132 valid := []string{ 133 "all_at_once", 134 "constraint", 135 "datacenters", 136 "group", 137 "id", 138 "meta", 139 "migrate", 140 "name", 141 "namespace", 142 "parameterized", 143 "periodic", 144 "priority", 145 "region", 146 "reschedule", 147 "task", 148 "type", 149 "update", 150 "vault", 151 "vault_token", 152 } 153 if err := helper.CheckHCLKeys(listVal, valid); err != nil { 154 return multierror.Prefix(err, "job:") 155 } 156 157 // Parse constraints 158 if o := listVal.Filter("constraint"); len(o.Items) > 0 { 159 if err := parseConstraints(&result.Constraints, o); err != nil { 160 return multierror.Prefix(err, "constraint ->") 161 } 162 } 163 164 // If we have an update strategy, then parse that 165 if o := listVal.Filter("update"); len(o.Items) > 0 { 166 if err := parseUpdate(&result.Update, o); err != nil { 167 return multierror.Prefix(err, "update ->") 168 } 169 } 170 171 // If we have a periodic definition, then parse that 172 if o := listVal.Filter("periodic"); len(o.Items) > 0 { 173 if err := parsePeriodic(&result.Periodic, o); err != nil { 174 return multierror.Prefix(err, "periodic ->") 175 } 176 } 177 178 // If we have a parameterized definition, then parse that 179 if o := listVal.Filter("parameterized"); len(o.Items) > 0 { 180 if err := parseParameterizedJob(&result.ParameterizedJob, o); err != nil { 181 return multierror.Prefix(err, "parameterized ->") 182 } 183 } 184 185 // If we have a reschedule stanza, then parse that 186 if o := listVal.Filter("reschedule"); len(o.Items) > 0 { 187 if err := parseReschedulePolicy(&result.Reschedule, o); err != nil { 188 return multierror.Prefix(err, "reschedule ->") 189 } 190 } 191 192 // If we have a migration strategy, then parse that 193 if o := listVal.Filter("migrate"); len(o.Items) > 0 { 194 if err := parseMigrate(&result.Migrate, o); err != nil { 195 return multierror.Prefix(err, "migrate ->") 196 } 197 } 198 199 // Parse out meta fields. These are in HCL as a list so we need 200 // to iterate over them and merge them. 201 if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 { 202 for _, o := range metaO.Elem().Items { 203 var m map[string]interface{} 204 if err := hcl.DecodeObject(&m, o.Val); err != nil { 205 return err 206 } 207 if err := mapstructure.WeakDecode(m, &result.Meta); err != nil { 208 return err 209 } 210 } 211 } 212 213 // If we have tasks outside, create TaskGroups for them 214 if o := listVal.Filter("task"); len(o.Items) > 0 { 215 var tasks []*api.Task 216 if err := parseTasks(*result.Name, "", &tasks, o); err != nil { 217 return multierror.Prefix(err, "task:") 218 } 219 220 result.TaskGroups = make([]*api.TaskGroup, len(tasks), len(tasks)*2) 221 for i, t := range tasks { 222 result.TaskGroups[i] = &api.TaskGroup{ 223 Name: helper.StringToPtr(t.Name), 224 Tasks: []*api.Task{t}, 225 } 226 } 227 } 228 229 // Parse the task groups 230 if o := listVal.Filter("group"); len(o.Items) > 0 { 231 if err := parseGroups(result, o); err != nil { 232 return multierror.Prefix(err, "group:") 233 } 234 } 235 236 // If we have a vault block, then parse that 237 if o := listVal.Filter("vault"); len(o.Items) > 0 { 238 jobVault := &api.Vault{ 239 Env: helper.BoolToPtr(true), 240 ChangeMode: helper.StringToPtr("restart"), 241 } 242 243 if err := parseVault(jobVault, o); err != nil { 244 return multierror.Prefix(err, "vault ->") 245 } 246 247 // Go through the task groups/tasks and if they don't have a Vault block, set it 248 for _, tg := range result.TaskGroups { 249 for _, task := range tg.Tasks { 250 if task.Vault == nil { 251 task.Vault = jobVault 252 } 253 } 254 } 255 } 256 257 return nil 258 } 259 260 func parseGroups(result *api.Job, list *ast.ObjectList) error { 261 list = list.Children() 262 if len(list.Items) == 0 { 263 return nil 264 } 265 266 // Go through each object and turn it into an actual result. 267 collection := make([]*api.TaskGroup, 0, len(list.Items)) 268 seen := make(map[string]struct{}) 269 for _, item := range list.Items { 270 n := item.Keys[0].Token.Value().(string) 271 272 // Make sure we haven't already found this 273 if _, ok := seen[n]; ok { 274 return fmt.Errorf("group '%s' defined more than once", n) 275 } 276 seen[n] = struct{}{} 277 278 // We need this later 279 var listVal *ast.ObjectList 280 if ot, ok := item.Val.(*ast.ObjectType); ok { 281 listVal = ot.List 282 } else { 283 return fmt.Errorf("group '%s': should be an object", n) 284 } 285 286 // Check for invalid keys 287 valid := []string{ 288 "count", 289 "constraint", 290 "restart", 291 "meta", 292 "task", 293 "ephemeral_disk", 294 "update", 295 "reschedule", 296 "vault", 297 "migrate", 298 } 299 if err := helper.CheckHCLKeys(listVal, valid); err != nil { 300 return multierror.Prefix(err, fmt.Sprintf("'%s' ->", n)) 301 } 302 303 var m map[string]interface{} 304 if err := hcl.DecodeObject(&m, item.Val); err != nil { 305 return err 306 } 307 delete(m, "constraint") 308 delete(m, "meta") 309 delete(m, "task") 310 delete(m, "restart") 311 delete(m, "ephemeral_disk") 312 delete(m, "update") 313 delete(m, "vault") 314 delete(m, "migrate") 315 316 // Build the group with the basic decode 317 var g api.TaskGroup 318 g.Name = helper.StringToPtr(n) 319 if err := mapstructure.WeakDecode(m, &g); err != nil { 320 return err 321 } 322 323 // Parse constraints 324 if o := listVal.Filter("constraint"); len(o.Items) > 0 { 325 if err := parseConstraints(&g.Constraints, o); err != nil { 326 return multierror.Prefix(err, fmt.Sprintf("'%s', constraint ->", n)) 327 } 328 } 329 330 // Parse restart policy 331 if o := listVal.Filter("restart"); len(o.Items) > 0 { 332 if err := parseRestartPolicy(&g.RestartPolicy, o); err != nil { 333 return multierror.Prefix(err, fmt.Sprintf("'%s', restart ->", n)) 334 } 335 } 336 337 // Parse reschedule policy 338 if o := listVal.Filter("reschedule"); len(o.Items) > 0 { 339 if err := parseReschedulePolicy(&g.ReschedulePolicy, o); err != nil { 340 return multierror.Prefix(err, fmt.Sprintf("'%s', reschedule ->", n)) 341 } 342 } 343 // Parse ephemeral disk 344 if o := listVal.Filter("ephemeral_disk"); len(o.Items) > 0 { 345 g.EphemeralDisk = &api.EphemeralDisk{} 346 if err := parseEphemeralDisk(&g.EphemeralDisk, o); err != nil { 347 return multierror.Prefix(err, fmt.Sprintf("'%s', ephemeral_disk ->", n)) 348 } 349 } 350 351 // If we have an update strategy, then parse that 352 if o := listVal.Filter("update"); len(o.Items) > 0 { 353 if err := parseUpdate(&g.Update, o); err != nil { 354 return multierror.Prefix(err, "update ->") 355 } 356 } 357 358 // If we have a migration strategy, then parse that 359 if o := listVal.Filter("migrate"); len(o.Items) > 0 { 360 if err := parseMigrate(&g.Migrate, o); err != nil { 361 return multierror.Prefix(err, "migrate ->") 362 } 363 } 364 365 // Parse out meta fields. These are in HCL as a list so we need 366 // to iterate over them and merge them. 367 if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 { 368 for _, o := range metaO.Elem().Items { 369 var m map[string]interface{} 370 if err := hcl.DecodeObject(&m, o.Val); err != nil { 371 return err 372 } 373 if err := mapstructure.WeakDecode(m, &g.Meta); err != nil { 374 return err 375 } 376 } 377 } 378 379 // Parse tasks 380 if o := listVal.Filter("task"); len(o.Items) > 0 { 381 if err := parseTasks(*result.Name, *g.Name, &g.Tasks, o); err != nil { 382 return multierror.Prefix(err, fmt.Sprintf("'%s', task:", n)) 383 } 384 } 385 386 // If we have a vault block, then parse that 387 if o := listVal.Filter("vault"); len(o.Items) > 0 { 388 tgVault := &api.Vault{ 389 Env: helper.BoolToPtr(true), 390 ChangeMode: helper.StringToPtr("restart"), 391 } 392 393 if err := parseVault(tgVault, o); err != nil { 394 return multierror.Prefix(err, fmt.Sprintf("'%s', vault ->", n)) 395 } 396 397 // Go through the tasks and if they don't have a Vault block, set it 398 for _, task := range g.Tasks { 399 if task.Vault == nil { 400 task.Vault = tgVault 401 } 402 } 403 } 404 405 collection = append(collection, &g) 406 } 407 408 result.TaskGroups = append(result.TaskGroups, collection...) 409 return nil 410 } 411 412 func parseRestartPolicy(final **api.RestartPolicy, list *ast.ObjectList) error { 413 list = list.Elem() 414 if len(list.Items) > 1 { 415 return fmt.Errorf("only one 'restart' block allowed") 416 } 417 418 // Get our job object 419 obj := list.Items[0] 420 421 // Check for invalid keys 422 valid := []string{ 423 "attempts", 424 "interval", 425 "delay", 426 "mode", 427 } 428 if err := helper.CheckHCLKeys(obj.Val, valid); err != nil { 429 return err 430 } 431 432 var m map[string]interface{} 433 if err := hcl.DecodeObject(&m, obj.Val); err != nil { 434 return err 435 } 436 437 var result api.RestartPolicy 438 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 439 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 440 WeaklyTypedInput: true, 441 Result: &result, 442 }) 443 if err != nil { 444 return err 445 } 446 if err := dec.Decode(m); err != nil { 447 return err 448 } 449 450 *final = &result 451 return nil 452 } 453 454 func parseReschedulePolicy(final **api.ReschedulePolicy, list *ast.ObjectList) error { 455 list = list.Elem() 456 if len(list.Items) > 1 { 457 return fmt.Errorf("only one 'reschedule' block allowed") 458 } 459 460 // Get our job object 461 obj := list.Items[0] 462 463 // Check for invalid keys 464 valid := []string{ 465 "attempts", 466 "interval", 467 "unlimited", 468 "delay", 469 "max_delay", 470 "delay_function", 471 } 472 if err := helper.CheckHCLKeys(obj.Val, valid); err != nil { 473 return err 474 } 475 476 var m map[string]interface{} 477 if err := hcl.DecodeObject(&m, obj.Val); err != nil { 478 return err 479 } 480 481 var result api.ReschedulePolicy 482 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 483 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 484 WeaklyTypedInput: true, 485 Result: &result, 486 }) 487 if err != nil { 488 return err 489 } 490 if err := dec.Decode(m); err != nil { 491 return err 492 } 493 494 *final = &result 495 return nil 496 } 497 498 func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error { 499 for _, o := range list.Elem().Items { 500 // Check for invalid keys 501 valid := []string{ 502 "attribute", 503 "distinct_hosts", 504 "distinct_property", 505 "operator", 506 "regexp", 507 "set_contains", 508 "value", 509 "version", 510 } 511 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 512 return err 513 } 514 515 var m map[string]interface{} 516 if err := hcl.DecodeObject(&m, o.Val); err != nil { 517 return err 518 } 519 520 m["LTarget"] = m["attribute"] 521 m["RTarget"] = m["value"] 522 m["Operand"] = m["operator"] 523 524 // If "version" is provided, set the operand 525 // to "version" and the value to the "RTarget" 526 if constraint, ok := m[structs.ConstraintVersion]; ok { 527 m["Operand"] = structs.ConstraintVersion 528 m["RTarget"] = constraint 529 } 530 531 // If "regexp" is provided, set the operand 532 // to "regexp" and the value to the "RTarget" 533 if constraint, ok := m[structs.ConstraintRegex]; ok { 534 m["Operand"] = structs.ConstraintRegex 535 m["RTarget"] = constraint 536 } 537 538 // If "set_contains" is provided, set the operand 539 // to "set_contains" and the value to the "RTarget" 540 if constraint, ok := m[structs.ConstraintSetContains]; ok { 541 m["Operand"] = structs.ConstraintSetContains 542 m["RTarget"] = constraint 543 } 544 545 if value, ok := m[structs.ConstraintDistinctHosts]; ok { 546 enabled, err := parseBool(value) 547 if err != nil { 548 return fmt.Errorf("distinct_hosts should be set to true or false; %v", err) 549 } 550 551 // If it is not enabled, skip the constraint. 552 if !enabled { 553 continue 554 } 555 556 m["Operand"] = structs.ConstraintDistinctHosts 557 } 558 559 if property, ok := m[structs.ConstraintDistinctProperty]; ok { 560 m["Operand"] = structs.ConstraintDistinctProperty 561 m["LTarget"] = property 562 } 563 564 // Build the constraint 565 var c api.Constraint 566 if err := mapstructure.WeakDecode(m, &c); err != nil { 567 return err 568 } 569 if c.Operand == "" { 570 c.Operand = "=" 571 } 572 573 *result = append(*result, &c) 574 } 575 576 return nil 577 } 578 579 func parseEphemeralDisk(result **api.EphemeralDisk, list *ast.ObjectList) error { 580 list = list.Elem() 581 if len(list.Items) > 1 { 582 return fmt.Errorf("only one 'ephemeral_disk' block allowed") 583 } 584 585 // Get our ephemeral_disk object 586 obj := list.Items[0] 587 588 // Check for invalid keys 589 valid := []string{ 590 "sticky", 591 "size", 592 "migrate", 593 } 594 if err := helper.CheckHCLKeys(obj.Val, valid); err != nil { 595 return err 596 } 597 598 var m map[string]interface{} 599 if err := hcl.DecodeObject(&m, obj.Val); err != nil { 600 return err 601 } 602 603 var ephemeralDisk api.EphemeralDisk 604 if err := mapstructure.WeakDecode(m, &ephemeralDisk); err != nil { 605 return err 606 } 607 *result = &ephemeralDisk 608 609 return nil 610 } 611 612 // parseBool takes an interface value and tries to convert it to a boolean and 613 // returns an error if the type can't be converted. 614 func parseBool(value interface{}) (bool, error) { 615 var enabled bool 616 var err error 617 switch value.(type) { 618 case string: 619 enabled, err = strconv.ParseBool(value.(string)) 620 case bool: 621 enabled = value.(bool) 622 default: 623 err = fmt.Errorf("%v couldn't be converted to boolean value", value) 624 } 625 626 return enabled, err 627 } 628 629 func parseTasks(jobName string, taskGroupName string, result *[]*api.Task, list *ast.ObjectList) error { 630 list = list.Children() 631 if len(list.Items) == 0 { 632 return nil 633 } 634 635 // Go through each object and turn it into an actual result. 636 seen := make(map[string]struct{}) 637 for _, item := range list.Items { 638 n := item.Keys[0].Token.Value().(string) 639 640 // Make sure we haven't already found this 641 if _, ok := seen[n]; ok { 642 return fmt.Errorf("task '%s' defined more than once", n) 643 } 644 seen[n] = struct{}{} 645 646 // We need this later 647 var listVal *ast.ObjectList 648 if ot, ok := item.Val.(*ast.ObjectType); ok { 649 listVal = ot.List 650 } else { 651 return fmt.Errorf("group '%s': should be an object", n) 652 } 653 654 // Check for invalid keys 655 valid := []string{ 656 "artifact", 657 "config", 658 "constraint", 659 "dispatch_payload", 660 "driver", 661 "env", 662 "kill_timeout", 663 "leader", 664 "logs", 665 "meta", 666 "resources", 667 "service", 668 "shutdown_delay", 669 "template", 670 "user", 671 "vault", 672 "kill_signal", 673 } 674 if err := helper.CheckHCLKeys(listVal, valid); err != nil { 675 return multierror.Prefix(err, fmt.Sprintf("'%s' ->", n)) 676 } 677 678 var m map[string]interface{} 679 if err := hcl.DecodeObject(&m, item.Val); err != nil { 680 return err 681 } 682 delete(m, "artifact") 683 delete(m, "config") 684 delete(m, "constraint") 685 delete(m, "dispatch_payload") 686 delete(m, "env") 687 delete(m, "logs") 688 delete(m, "meta") 689 delete(m, "resources") 690 delete(m, "service") 691 delete(m, "template") 692 delete(m, "vault") 693 694 // Build the task 695 var t api.Task 696 t.Name = n 697 if taskGroupName == "" { 698 taskGroupName = n 699 } 700 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 701 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 702 WeaklyTypedInput: true, 703 Result: &t, 704 }) 705 706 if err != nil { 707 return err 708 } 709 if err := dec.Decode(m); err != nil { 710 return err 711 } 712 713 // If we have env, then parse them 714 if o := listVal.Filter("env"); len(o.Items) > 0 { 715 for _, o := range o.Elem().Items { 716 var m map[string]interface{} 717 if err := hcl.DecodeObject(&m, o.Val); err != nil { 718 return err 719 } 720 if err := mapstructure.WeakDecode(m, &t.Env); err != nil { 721 return err 722 } 723 } 724 } 725 726 if o := listVal.Filter("service"); len(o.Items) > 0 { 727 if err := parseServices(jobName, taskGroupName, &t, o); err != nil { 728 return multierror.Prefix(err, fmt.Sprintf("'%s',", n)) 729 } 730 } 731 732 // If we have config, then parse that 733 if o := listVal.Filter("config"); len(o.Items) > 0 { 734 for _, o := range o.Elem().Items { 735 var m map[string]interface{} 736 if err := hcl.DecodeObject(&m, o.Val); err != nil { 737 return err 738 } 739 740 if err := mapstructure.WeakDecode(m, &t.Config); err != nil { 741 return err 742 } 743 } 744 } 745 746 // Parse constraints 747 if o := listVal.Filter("constraint"); len(o.Items) > 0 { 748 if err := parseConstraints(&t.Constraints, o); err != nil { 749 return multierror.Prefix(err, fmt.Sprintf( 750 "'%s', constraint ->", n)) 751 } 752 } 753 754 // Parse out meta fields. These are in HCL as a list so we need 755 // to iterate over them and merge them. 756 if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 { 757 for _, o := range metaO.Elem().Items { 758 var m map[string]interface{} 759 if err := hcl.DecodeObject(&m, o.Val); err != nil { 760 return err 761 } 762 if err := mapstructure.WeakDecode(m, &t.Meta); err != nil { 763 return err 764 } 765 } 766 } 767 768 // If we have resources, then parse that 769 if o := listVal.Filter("resources"); len(o.Items) > 0 { 770 var r api.Resources 771 if err := parseResources(&r, o); err != nil { 772 return multierror.Prefix(err, fmt.Sprintf("'%s',", n)) 773 } 774 775 t.Resources = &r 776 } 777 778 // If we have logs then parse that 779 if o := listVal.Filter("logs"); len(o.Items) > 0 { 780 if len(o.Items) > 1 { 781 return fmt.Errorf("only one logs block is allowed in a Task. Number of logs block found: %d", len(o.Items)) 782 } 783 var m map[string]interface{} 784 logsBlock := o.Items[0] 785 786 // Check for invalid keys 787 valid := []string{ 788 "max_files", 789 "max_file_size", 790 } 791 if err := helper.CheckHCLKeys(logsBlock.Val, valid); err != nil { 792 return multierror.Prefix(err, fmt.Sprintf("'%s', logs ->", n)) 793 } 794 795 if err := hcl.DecodeObject(&m, logsBlock.Val); err != nil { 796 return err 797 } 798 799 var log api.LogConfig 800 if err := mapstructure.WeakDecode(m, &log); err != nil { 801 return err 802 } 803 804 t.LogConfig = &log 805 } 806 807 // Parse artifacts 808 if o := listVal.Filter("artifact"); len(o.Items) > 0 { 809 if err := parseArtifacts(&t.Artifacts, o); err != nil { 810 return multierror.Prefix(err, fmt.Sprintf("'%s', artifact ->", n)) 811 } 812 } 813 814 // Parse templates 815 if o := listVal.Filter("template"); len(o.Items) > 0 { 816 if err := parseTemplates(&t.Templates, o); err != nil { 817 return multierror.Prefix(err, fmt.Sprintf("'%s', template ->", n)) 818 } 819 } 820 821 // If we have a vault block, then parse that 822 if o := listVal.Filter("vault"); len(o.Items) > 0 { 823 v := &api.Vault{ 824 Env: helper.BoolToPtr(true), 825 ChangeMode: helper.StringToPtr("restart"), 826 } 827 828 if err := parseVault(v, o); err != nil { 829 return multierror.Prefix(err, fmt.Sprintf("'%s', vault ->", n)) 830 } 831 832 t.Vault = v 833 } 834 835 // If we have a dispatch_payload block parse that 836 if o := listVal.Filter("dispatch_payload"); len(o.Items) > 0 { 837 if len(o.Items) > 1 { 838 return fmt.Errorf("only one dispatch_payload block is allowed in a task. Number of dispatch_payload blocks found: %d", len(o.Items)) 839 } 840 var m map[string]interface{} 841 dispatchBlock := o.Items[0] 842 843 // Check for invalid keys 844 valid := []string{ 845 "file", 846 } 847 if err := helper.CheckHCLKeys(dispatchBlock.Val, valid); err != nil { 848 return multierror.Prefix(err, fmt.Sprintf("'%s', dispatch_payload ->", n)) 849 } 850 851 if err := hcl.DecodeObject(&m, dispatchBlock.Val); err != nil { 852 return err 853 } 854 855 t.DispatchPayload = &api.DispatchPayloadConfig{} 856 if err := mapstructure.WeakDecode(m, t.DispatchPayload); err != nil { 857 return err 858 } 859 } 860 861 *result = append(*result, &t) 862 } 863 864 return nil 865 } 866 867 func parseArtifacts(result *[]*api.TaskArtifact, list *ast.ObjectList) error { 868 for _, o := range list.Elem().Items { 869 // Check for invalid keys 870 valid := []string{ 871 "source", 872 "options", 873 "mode", 874 "destination", 875 } 876 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 877 return err 878 } 879 880 var m map[string]interface{} 881 if err := hcl.DecodeObject(&m, o.Val); err != nil { 882 return err 883 } 884 885 delete(m, "options") 886 887 var ta api.TaskArtifact 888 if err := mapstructure.WeakDecode(m, &ta); err != nil { 889 return err 890 } 891 892 var optionList *ast.ObjectList 893 if ot, ok := o.Val.(*ast.ObjectType); ok { 894 optionList = ot.List 895 } else { 896 return fmt.Errorf("artifact should be an object") 897 } 898 899 if oo := optionList.Filter("options"); len(oo.Items) > 0 { 900 options := make(map[string]string) 901 if err := parseArtifactOption(options, oo); err != nil { 902 return multierror.Prefix(err, "options: ") 903 } 904 ta.GetterOptions = options 905 } 906 907 *result = append(*result, &ta) 908 } 909 910 return nil 911 } 912 913 func parseArtifactOption(result map[string]string, list *ast.ObjectList) error { 914 list = list.Elem() 915 if len(list.Items) > 1 { 916 return fmt.Errorf("only one 'options' block allowed per artifact") 917 } 918 919 // Get our resource object 920 o := list.Items[0] 921 922 var m map[string]interface{} 923 if err := hcl.DecodeObject(&m, o.Val); err != nil { 924 return err 925 } 926 927 if err := mapstructure.WeakDecode(m, &result); err != nil { 928 return err 929 } 930 931 return nil 932 } 933 934 func parseTemplates(result *[]*api.Template, list *ast.ObjectList) error { 935 for _, o := range list.Elem().Items { 936 // Check for invalid keys 937 valid := []string{ 938 "change_mode", 939 "change_signal", 940 "data", 941 "destination", 942 "left_delimiter", 943 "perms", 944 "right_delimiter", 945 "source", 946 "splay", 947 "env", 948 "vault_grace", 949 } 950 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 951 return err 952 } 953 954 var m map[string]interface{} 955 if err := hcl.DecodeObject(&m, o.Val); err != nil { 956 return err 957 } 958 959 templ := &api.Template{ 960 ChangeMode: helper.StringToPtr("restart"), 961 Splay: helper.TimeToPtr(5 * time.Second), 962 Perms: helper.StringToPtr("0644"), 963 } 964 965 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 966 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 967 WeaklyTypedInput: true, 968 Result: templ, 969 }) 970 if err != nil { 971 return err 972 } 973 if err := dec.Decode(m); err != nil { 974 return err 975 } 976 977 *result = append(*result, templ) 978 } 979 980 return nil 981 } 982 983 func parseServices(jobName string, taskGroupName string, task *api.Task, serviceObjs *ast.ObjectList) error { 984 task.Services = make([]*api.Service, len(serviceObjs.Items)) 985 for idx, o := range serviceObjs.Items { 986 // Check for invalid keys 987 valid := []string{ 988 "name", 989 "tags", 990 "port", 991 "check", 992 "address_mode", 993 "check_restart", 994 } 995 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 996 return multierror.Prefix(err, fmt.Sprintf("service (%d) ->", idx)) 997 } 998 999 var service api.Service 1000 var m map[string]interface{} 1001 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1002 return err 1003 } 1004 1005 delete(m, "check") 1006 delete(m, "check_restart") 1007 1008 if err := mapstructure.WeakDecode(m, &service); err != nil { 1009 return err 1010 } 1011 1012 // Filter checks 1013 var checkList *ast.ObjectList 1014 if ot, ok := o.Val.(*ast.ObjectType); ok { 1015 checkList = ot.List 1016 } else { 1017 return fmt.Errorf("service '%s': should be an object", service.Name) 1018 } 1019 1020 if co := checkList.Filter("check"); len(co.Items) > 0 { 1021 if err := parseChecks(&service, co); err != nil { 1022 return multierror.Prefix(err, fmt.Sprintf("service: '%s',", service.Name)) 1023 } 1024 } 1025 1026 // Filter check_restart 1027 if cro := checkList.Filter("check_restart"); len(cro.Items) > 0 { 1028 if len(cro.Items) > 1 { 1029 return fmt.Errorf("check_restart '%s': cannot have more than 1 check_restart", service.Name) 1030 } 1031 if cr, err := parseCheckRestart(cro.Items[0]); err != nil { 1032 return multierror.Prefix(err, fmt.Sprintf("service: '%s',", service.Name)) 1033 } else { 1034 service.CheckRestart = cr 1035 } 1036 } 1037 1038 task.Services[idx] = &service 1039 } 1040 1041 return nil 1042 } 1043 1044 func parseChecks(service *api.Service, checkObjs *ast.ObjectList) error { 1045 service.Checks = make([]api.ServiceCheck, len(checkObjs.Items)) 1046 for idx, co := range checkObjs.Items { 1047 // Check for invalid keys 1048 valid := []string{ 1049 "name", 1050 "type", 1051 "interval", 1052 "timeout", 1053 "path", 1054 "protocol", 1055 "port", 1056 "command", 1057 "args", 1058 "initial_status", 1059 "tls_skip_verify", 1060 "header", 1061 "method", 1062 "check_restart", 1063 "address_mode", 1064 } 1065 if err := helper.CheckHCLKeys(co.Val, valid); err != nil { 1066 return multierror.Prefix(err, "check ->") 1067 } 1068 1069 var check api.ServiceCheck 1070 var cm map[string]interface{} 1071 if err := hcl.DecodeObject(&cm, co.Val); err != nil { 1072 return err 1073 } 1074 1075 // HCL allows repeating stanzas so merge 'header' into a single 1076 // map[string][]string. 1077 if headerI, ok := cm["header"]; ok { 1078 headerRaw, ok := headerI.([]map[string]interface{}) 1079 if !ok { 1080 return fmt.Errorf("check -> header -> expected a []map[string][]string but found %T", headerI) 1081 } 1082 m := map[string][]string{} 1083 for _, rawm := range headerRaw { 1084 for k, vI := range rawm { 1085 vs, ok := vI.([]interface{}) 1086 if !ok { 1087 return fmt.Errorf("check -> header -> %q expected a []string but found %T", k, vI) 1088 } 1089 for _, vI := range vs { 1090 v, ok := vI.(string) 1091 if !ok { 1092 return fmt.Errorf("check -> header -> %q expected a string but found %T", k, vI) 1093 } 1094 m[k] = append(m[k], v) 1095 } 1096 } 1097 } 1098 1099 check.Header = m 1100 1101 // Remove "header" as it has been parsed 1102 delete(cm, "header") 1103 } 1104 1105 delete(cm, "check_restart") 1106 1107 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 1108 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 1109 WeaklyTypedInput: true, 1110 Result: &check, 1111 }) 1112 if err != nil { 1113 return err 1114 } 1115 if err := dec.Decode(cm); err != nil { 1116 return err 1117 } 1118 1119 // Filter check_restart 1120 var checkRestartList *ast.ObjectList 1121 if ot, ok := co.Val.(*ast.ObjectType); ok { 1122 checkRestartList = ot.List 1123 } else { 1124 return fmt.Errorf("check_restart '%s': should be an object", check.Name) 1125 } 1126 1127 if cro := checkRestartList.Filter("check_restart"); len(cro.Items) > 0 { 1128 if len(cro.Items) > 1 { 1129 return fmt.Errorf("check_restart '%s': cannot have more than 1 check_restart", check.Name) 1130 } 1131 if cr, err := parseCheckRestart(cro.Items[0]); err != nil { 1132 return multierror.Prefix(err, fmt.Sprintf("check: '%s',", check.Name)) 1133 } else { 1134 check.CheckRestart = cr 1135 } 1136 } 1137 1138 service.Checks[idx] = check 1139 } 1140 1141 return nil 1142 } 1143 1144 func parseCheckRestart(cro *ast.ObjectItem) (*api.CheckRestart, error) { 1145 valid := []string{ 1146 "limit", 1147 "grace", 1148 "ignore_warnings", 1149 } 1150 1151 if err := helper.CheckHCLKeys(cro.Val, valid); err != nil { 1152 return nil, multierror.Prefix(err, "check_restart ->") 1153 } 1154 1155 var checkRestart api.CheckRestart 1156 var crm map[string]interface{} 1157 if err := hcl.DecodeObject(&crm, cro.Val); err != nil { 1158 return nil, err 1159 } 1160 1161 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 1162 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 1163 WeaklyTypedInput: true, 1164 Result: &checkRestart, 1165 }) 1166 if err != nil { 1167 return nil, err 1168 } 1169 if err := dec.Decode(crm); err != nil { 1170 return nil, err 1171 } 1172 1173 return &checkRestart, nil 1174 } 1175 1176 func parseResources(result *api.Resources, list *ast.ObjectList) error { 1177 list = list.Elem() 1178 if len(list.Items) == 0 { 1179 return nil 1180 } 1181 if len(list.Items) > 1 { 1182 return fmt.Errorf("only one 'resource' block allowed per task") 1183 } 1184 1185 // Get our resource object 1186 o := list.Items[0] 1187 1188 // We need this later 1189 var listVal *ast.ObjectList 1190 if ot, ok := o.Val.(*ast.ObjectType); ok { 1191 listVal = ot.List 1192 } else { 1193 return fmt.Errorf("resource: should be an object") 1194 } 1195 1196 // Check for invalid keys 1197 valid := []string{ 1198 "cpu", 1199 "iops", 1200 "disk", 1201 "memory", 1202 "network", 1203 } 1204 if err := helper.CheckHCLKeys(listVal, valid); err != nil { 1205 return multierror.Prefix(err, "resources ->") 1206 } 1207 1208 var m map[string]interface{} 1209 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1210 return err 1211 } 1212 delete(m, "network") 1213 1214 if err := mapstructure.WeakDecode(m, result); err != nil { 1215 return err 1216 } 1217 1218 // Parse the network resources 1219 if o := listVal.Filter("network"); len(o.Items) > 0 { 1220 if len(o.Items) > 1 { 1221 return fmt.Errorf("only one 'network' resource allowed") 1222 } 1223 1224 // Check for invalid keys 1225 valid := []string{ 1226 "mbits", 1227 "port", 1228 } 1229 if err := helper.CheckHCLKeys(o.Items[0].Val, valid); err != nil { 1230 return multierror.Prefix(err, "resources, network ->") 1231 } 1232 1233 var r api.NetworkResource 1234 var m map[string]interface{} 1235 if err := hcl.DecodeObject(&m, o.Items[0].Val); err != nil { 1236 return err 1237 } 1238 if err := mapstructure.WeakDecode(m, &r); err != nil { 1239 return err 1240 } 1241 1242 var networkObj *ast.ObjectList 1243 if ot, ok := o.Items[0].Val.(*ast.ObjectType); ok { 1244 networkObj = ot.List 1245 } else { 1246 return fmt.Errorf("resource: should be an object") 1247 } 1248 if err := parsePorts(networkObj, &r); err != nil { 1249 return multierror.Prefix(err, "resources, network, ports ->") 1250 } 1251 1252 result.Networks = []*api.NetworkResource{&r} 1253 } 1254 1255 return nil 1256 } 1257 1258 func parsePorts(networkObj *ast.ObjectList, nw *api.NetworkResource) error { 1259 // Check for invalid keys 1260 valid := []string{ 1261 "mbits", 1262 "port", 1263 } 1264 if err := helper.CheckHCLKeys(networkObj, valid); err != nil { 1265 return err 1266 } 1267 1268 portsObjList := networkObj.Filter("port") 1269 knownPortLabels := make(map[string]bool) 1270 for _, port := range portsObjList.Items { 1271 if len(port.Keys) == 0 { 1272 return fmt.Errorf("ports must be named") 1273 } 1274 label := port.Keys[0].Token.Value().(string) 1275 if !reDynamicPorts.MatchString(label) { 1276 return errPortLabel 1277 } 1278 l := strings.ToLower(label) 1279 if knownPortLabels[l] { 1280 return fmt.Errorf("found a port label collision: %s", label) 1281 } 1282 var p map[string]interface{} 1283 var res api.Port 1284 if err := hcl.DecodeObject(&p, port.Val); err != nil { 1285 return err 1286 } 1287 if err := mapstructure.WeakDecode(p, &res); err != nil { 1288 return err 1289 } 1290 res.Label = label 1291 if res.Value > 0 { 1292 nw.ReservedPorts = append(nw.ReservedPorts, res) 1293 } else { 1294 nw.DynamicPorts = append(nw.DynamicPorts, res) 1295 } 1296 knownPortLabels[l] = true 1297 } 1298 return nil 1299 } 1300 1301 func parseUpdate(result **api.UpdateStrategy, list *ast.ObjectList) error { 1302 list = list.Elem() 1303 if len(list.Items) > 1 { 1304 return fmt.Errorf("only one 'update' block allowed") 1305 } 1306 1307 // Get our resource object 1308 o := list.Items[0] 1309 1310 var m map[string]interface{} 1311 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1312 return err 1313 } 1314 1315 // Check for invalid keys 1316 valid := []string{ 1317 // COMPAT: Remove in 0.7.0. Stagger is deprecated in 0.6.0. 1318 "stagger", 1319 "max_parallel", 1320 "health_check", 1321 "min_healthy_time", 1322 "healthy_deadline", 1323 "auto_revert", 1324 "canary", 1325 } 1326 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 1327 return err 1328 } 1329 1330 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 1331 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 1332 WeaklyTypedInput: true, 1333 Result: result, 1334 }) 1335 if err != nil { 1336 return err 1337 } 1338 return dec.Decode(m) 1339 } 1340 1341 func parseMigrate(result **api.MigrateStrategy, list *ast.ObjectList) error { 1342 list = list.Elem() 1343 if len(list.Items) > 1 { 1344 return fmt.Errorf("only one 'migrate' block allowed") 1345 } 1346 1347 // Get our resource object 1348 o := list.Items[0] 1349 1350 var m map[string]interface{} 1351 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1352 return err 1353 } 1354 1355 // Check for invalid keys 1356 valid := []string{ 1357 "max_parallel", 1358 "health_check", 1359 "min_healthy_time", 1360 "healthy_deadline", 1361 } 1362 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 1363 return err 1364 } 1365 1366 dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 1367 DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 1368 WeaklyTypedInput: true, 1369 Result: result, 1370 }) 1371 if err != nil { 1372 return err 1373 } 1374 return dec.Decode(m) 1375 } 1376 1377 func parsePeriodic(result **api.PeriodicConfig, list *ast.ObjectList) error { 1378 list = list.Elem() 1379 if len(list.Items) > 1 { 1380 return fmt.Errorf("only one 'periodic' block allowed per job") 1381 } 1382 1383 // Get our resource object 1384 o := list.Items[0] 1385 1386 var m map[string]interface{} 1387 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1388 return err 1389 } 1390 1391 // Check for invalid keys 1392 valid := []string{ 1393 "enabled", 1394 "cron", 1395 "prohibit_overlap", 1396 "time_zone", 1397 } 1398 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 1399 return err 1400 } 1401 1402 if value, ok := m["enabled"]; ok { 1403 enabled, err := parseBool(value) 1404 if err != nil { 1405 return fmt.Errorf("periodic.enabled should be set to true or false; %v", err) 1406 } 1407 m["Enabled"] = enabled 1408 } 1409 1410 // If "cron" is provided, set the type to "cron" and store the spec. 1411 if cron, ok := m["cron"]; ok { 1412 m["SpecType"] = structs.PeriodicSpecCron 1413 m["Spec"] = cron 1414 } 1415 1416 // Build the constraint 1417 var p api.PeriodicConfig 1418 if err := mapstructure.WeakDecode(m, &p); err != nil { 1419 return err 1420 } 1421 *result = &p 1422 return nil 1423 } 1424 1425 func parseVault(result *api.Vault, list *ast.ObjectList) error { 1426 list = list.Elem() 1427 if len(list.Items) == 0 { 1428 return nil 1429 } 1430 if len(list.Items) > 1 { 1431 return fmt.Errorf("only one 'vault' block allowed per task") 1432 } 1433 1434 // Get our resource object 1435 o := list.Items[0] 1436 1437 // We need this later 1438 var listVal *ast.ObjectList 1439 if ot, ok := o.Val.(*ast.ObjectType); ok { 1440 listVal = ot.List 1441 } else { 1442 return fmt.Errorf("vault: should be an object") 1443 } 1444 1445 // Check for invalid keys 1446 valid := []string{ 1447 "policies", 1448 "env", 1449 "change_mode", 1450 "change_signal", 1451 } 1452 if err := helper.CheckHCLKeys(listVal, valid); err != nil { 1453 return multierror.Prefix(err, "vault ->") 1454 } 1455 1456 var m map[string]interface{} 1457 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1458 return err 1459 } 1460 1461 if err := mapstructure.WeakDecode(m, result); err != nil { 1462 return err 1463 } 1464 1465 return nil 1466 } 1467 1468 func parseParameterizedJob(result **api.ParameterizedJobConfig, list *ast.ObjectList) error { 1469 list = list.Elem() 1470 if len(list.Items) > 1 { 1471 return fmt.Errorf("only one 'parameterized' block allowed per job") 1472 } 1473 1474 // Get our resource object 1475 o := list.Items[0] 1476 1477 var m map[string]interface{} 1478 if err := hcl.DecodeObject(&m, o.Val); err != nil { 1479 return err 1480 } 1481 1482 // Check for invalid keys 1483 valid := []string{ 1484 "payload", 1485 "meta_required", 1486 "meta_optional", 1487 } 1488 if err := helper.CheckHCLKeys(o.Val, valid); err != nil { 1489 return err 1490 } 1491 1492 // Build the parameterized job block 1493 var d api.ParameterizedJobConfig 1494 if err := mapstructure.WeakDecode(m, &d); err != nil { 1495 return err 1496 } 1497 1498 *result = &d 1499 return nil 1500 }