7936c1e33f
This complements the `env` parameter, so that the operator can author tasks that don't share their Vault token with the workload when using `image` filesystem isolation. As a result, more powerful tokens can be used in a job definition, allowing it to use template stanzas to issue all kinds of secrets (database secrets, Vault tokens with very specific policies, etc.), without sharing that issuing power with the task itself. This is accomplished by creating a directory called `private` within the task's working directory, which shares many properties of the `secrets` directory (tmpfs where possible, not accessible by `nomad alloc fs` or Nomad's web UI), but isn't mounted into/bound to the container. If the `disable_file` parameter is set to `false` (its default), the Vault token is also written to the NOMAD_SECRETS_DIR, so the default behavior is backwards compatible. Even if the operator never changes the default, they will still benefit from the improved behavior of Nomad never reading the token back in from that - potentially altered - location.
533 lines
12 KiB
Go
533 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package jobspec
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/hashicorp/nomad/api"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
var reDynamicPorts = regexp.MustCompile("^[a-zA-Z0-9_]+$")
|
|
var errPortLabel = fmt.Errorf("Port label does not conform to naming requirements %s", reDynamicPorts.String())
|
|
|
|
// Parse parses the job spec from the given io.Reader.
|
|
//
|
|
// Due to current internal limitations, the entire contents of the
|
|
// io.Reader will be copied into memory first before parsing.
|
|
func Parse(r io.Reader) (*api.Job, error) {
|
|
// Copy the reader into an in-memory buffer first since HCL requires it.
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, r); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the buffer
|
|
root, err := hcl.Parse(buf.String())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing: %s", err)
|
|
}
|
|
buf.Reset()
|
|
|
|
// Top-level item should be a list
|
|
list, ok := root.Node.(*ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing: root should be an object")
|
|
}
|
|
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"job",
|
|
}
|
|
if err := checkHCLKeys(list, valid); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var job api.Job
|
|
|
|
// Parse the job out
|
|
matches := list.Filter("job")
|
|
if len(matches.Items) == 0 {
|
|
return nil, fmt.Errorf("'job' block not found")
|
|
}
|
|
if err := parseJob(&job, matches); err != nil {
|
|
return nil, fmt.Errorf("error parsing 'job': %s", err)
|
|
}
|
|
|
|
return &job, nil
|
|
}
|
|
|
|
// ParseFile parses the given path as a job spec.
|
|
func ParseFile(path string) (*api.Job, error) {
|
|
path, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
return Parse(f)
|
|
}
|
|
|
|
func parseReschedulePolicy(final **api.ReschedulePolicy, list *ast.ObjectList) error {
|
|
list = list.Elem()
|
|
if len(list.Items) > 1 {
|
|
return fmt.Errorf("only one 'reschedule' block allowed")
|
|
}
|
|
|
|
// Get our job object
|
|
obj := list.Items[0]
|
|
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"attempts",
|
|
"interval",
|
|
"unlimited",
|
|
"delay",
|
|
"max_delay",
|
|
"delay_function",
|
|
}
|
|
if err := checkHCLKeys(obj.Val, valid); err != nil {
|
|
return err
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, obj.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
var result api.ReschedulePolicy
|
|
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
|
WeaklyTypedInput: true,
|
|
Result: &result,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dec.Decode(m); err != nil {
|
|
return err
|
|
}
|
|
|
|
*final = &result
|
|
return nil
|
|
}
|
|
|
|
func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error {
|
|
for _, o := range list.Elem().Items {
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"attribute",
|
|
"distinct_hosts",
|
|
"distinct_property",
|
|
"operator",
|
|
"regexp",
|
|
"set_contains",
|
|
"value",
|
|
"version",
|
|
"semver",
|
|
}
|
|
if err := checkHCLKeys(o.Val, valid); err != nil {
|
|
return err
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
m["LTarget"] = m["attribute"]
|
|
m["RTarget"] = m["value"]
|
|
m["Operand"] = m["operator"]
|
|
|
|
// If "version" is provided, set the operand
|
|
// to "version" and the value to the "RTarget"
|
|
if constraint, ok := m[api.ConstraintVersion]; ok {
|
|
m["Operand"] = api.ConstraintVersion
|
|
m["RTarget"] = constraint
|
|
}
|
|
|
|
// If "semver" is provided, set the operand
|
|
// to "semver" and the value to the "RTarget"
|
|
if constraint, ok := m[api.ConstraintSemver]; ok {
|
|
m["Operand"] = api.ConstraintSemver
|
|
m["RTarget"] = constraint
|
|
}
|
|
|
|
// If "regexp" is provided, set the operand
|
|
// to "regexp" and the value to the "RTarget"
|
|
if constraint, ok := m[api.ConstraintRegex]; ok {
|
|
m["Operand"] = api.ConstraintRegex
|
|
m["RTarget"] = constraint
|
|
}
|
|
|
|
// If "set_contains" is provided, set the operand
|
|
// to "set_contains" and the value to the "RTarget"
|
|
if constraint, ok := m[api.ConstraintSetContains]; ok {
|
|
m["Operand"] = api.ConstraintSetContains
|
|
m["RTarget"] = constraint
|
|
}
|
|
|
|
if value, ok := m[api.ConstraintDistinctHosts]; ok {
|
|
enabled, err := parseBool(value)
|
|
if err != nil {
|
|
return fmt.Errorf("distinct_hosts should be set to true or false; %v", err)
|
|
}
|
|
|
|
// If it is not enabled, skip the constraint.
|
|
if !enabled {
|
|
continue
|
|
}
|
|
|
|
m["Operand"] = api.ConstraintDistinctHosts
|
|
}
|
|
|
|
if property, ok := m[api.ConstraintDistinctProperty]; ok {
|
|
m["Operand"] = api.ConstraintDistinctProperty
|
|
m["LTarget"] = property
|
|
}
|
|
|
|
// Build the constraint
|
|
var c api.Constraint
|
|
if err := mapstructure.WeakDecode(m, &c); err != nil {
|
|
return err
|
|
}
|
|
if c.Operand == "" {
|
|
c.Operand = "="
|
|
}
|
|
|
|
*result = append(*result, &c)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseAffinities(result *[]*api.Affinity, list *ast.ObjectList) error {
|
|
for _, o := range list.Elem().Items {
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"attribute",
|
|
"operator",
|
|
"regexp",
|
|
"set_contains",
|
|
"set_contains_any",
|
|
"set_contains_all",
|
|
"value",
|
|
"version",
|
|
"semver",
|
|
"weight",
|
|
}
|
|
if err := checkHCLKeys(o.Val, valid); err != nil {
|
|
return err
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
m["LTarget"] = m["attribute"]
|
|
m["RTarget"] = m["value"]
|
|
m["Operand"] = m["operator"]
|
|
|
|
// If "version" is provided, set the operand
|
|
// to "version" and the value to the "RTarget"
|
|
if affinity, ok := m[api.ConstraintVersion]; ok {
|
|
m["Operand"] = api.ConstraintVersion
|
|
m["RTarget"] = affinity
|
|
}
|
|
|
|
// If "semver" is provided, set the operand
|
|
// to "semver" and the value to the "RTarget"
|
|
if affinity, ok := m[api.ConstraintSemver]; ok {
|
|
m["Operand"] = api.ConstraintSemver
|
|
m["RTarget"] = affinity
|
|
}
|
|
|
|
// If "regexp" is provided, set the operand
|
|
// to "regexp" and the value to the "RTarget"
|
|
if affinity, ok := m[api.ConstraintRegex]; ok {
|
|
m["Operand"] = api.ConstraintRegex
|
|
m["RTarget"] = affinity
|
|
}
|
|
|
|
// If "set_contains_any" is provided, set the operand
|
|
// to "set_contains_any" and the value to the "RTarget"
|
|
if affinity, ok := m[api.ConstraintSetContainsAny]; ok {
|
|
m["Operand"] = api.ConstraintSetContainsAny
|
|
m["RTarget"] = affinity
|
|
}
|
|
|
|
// If "set_contains_all" is provided, set the operand
|
|
// to "set_contains_all" and the value to the "RTarget"
|
|
if affinity, ok := m[api.ConstraintSetContainsAll]; ok {
|
|
m["Operand"] = api.ConstraintSetContainsAll
|
|
m["RTarget"] = affinity
|
|
}
|
|
|
|
// set_contains is a synonym of set_contains_all
|
|
if affinity, ok := m[api.ConstraintSetContains]; ok {
|
|
m["Operand"] = api.ConstraintSetContains
|
|
m["RTarget"] = affinity
|
|
}
|
|
|
|
// Build the affinity
|
|
var a api.Affinity
|
|
if err := mapstructure.WeakDecode(m, &a); err != nil {
|
|
return err
|
|
}
|
|
if a.Operand == "" {
|
|
a.Operand = "="
|
|
}
|
|
|
|
*result = append(*result, &a)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseSpread(result *[]*api.Spread, list *ast.ObjectList) error {
|
|
for _, o := range list.Elem().Items {
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"attribute",
|
|
"weight",
|
|
"target",
|
|
}
|
|
if err := checkHCLKeys(o.Val, valid); err != nil {
|
|
return err
|
|
}
|
|
|
|
// We need this later
|
|
var listVal *ast.ObjectList
|
|
if ot, ok := o.Val.(*ast.ObjectType); ok {
|
|
listVal = ot.List
|
|
} else {
|
|
return fmt.Errorf("spread should be an object")
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
|
return err
|
|
}
|
|
delete(m, "target")
|
|
// Build spread
|
|
var s api.Spread
|
|
if err := mapstructure.WeakDecode(m, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse spread target
|
|
if o := listVal.Filter("target"); len(o.Items) > 0 {
|
|
if err := parseSpreadTarget(&s.SpreadTarget, o); err != nil {
|
|
return multierror.Prefix(err, "target ->")
|
|
}
|
|
}
|
|
|
|
*result = append(*result, &s)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseSpreadTarget(result *[]*api.SpreadTarget, list *ast.ObjectList) error {
|
|
seen := make(map[string]struct{})
|
|
for _, item := range list.Items {
|
|
if len(item.Keys) != 1 {
|
|
return fmt.Errorf("missing spread target")
|
|
}
|
|
n := item.Keys[0].Token.Value().(string)
|
|
|
|
// Make sure we haven't already found this
|
|
if _, ok := seen[n]; ok {
|
|
return fmt.Errorf("target '%s' defined more than once", n)
|
|
}
|
|
seen[n] = struct{}{}
|
|
|
|
// We need this later
|
|
var listVal *ast.ObjectList
|
|
if ot, ok := item.Val.(*ast.ObjectType); ok {
|
|
listVal = ot.List
|
|
} else {
|
|
return fmt.Errorf("target should be an object")
|
|
}
|
|
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"percent",
|
|
"value",
|
|
}
|
|
if err := checkHCLKeys(listVal, valid); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("'%s' ->", n))
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, item.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Decode spread target
|
|
var g api.SpreadTarget
|
|
g.Value = n
|
|
if err := mapstructure.WeakDecode(m, &g); err != nil {
|
|
return err
|
|
}
|
|
*result = append(*result, &g)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseBool takes an interface value and tries to convert it to a boolean and
|
|
// returns an error if the type can't be converted.
|
|
func parseBool(value interface{}) (bool, error) {
|
|
var enabled bool
|
|
var err error
|
|
switch data := value.(type) {
|
|
case string:
|
|
enabled, err = strconv.ParseBool(data)
|
|
case bool:
|
|
enabled = data
|
|
default:
|
|
err = fmt.Errorf("%v couldn't be converted to boolean value", value)
|
|
}
|
|
|
|
return enabled, err
|
|
}
|
|
|
|
func parseUpdate(result **api.UpdateStrategy, list *ast.ObjectList) error {
|
|
list = list.Elem()
|
|
if len(list.Items) > 1 {
|
|
return fmt.Errorf("only one 'update' block allowed")
|
|
}
|
|
|
|
// Get our resource object
|
|
o := list.Items[0]
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"stagger",
|
|
"max_parallel",
|
|
"health_check",
|
|
"min_healthy_time",
|
|
"healthy_deadline",
|
|
"progress_deadline",
|
|
"auto_revert",
|
|
"auto_promote",
|
|
"canary",
|
|
}
|
|
if err := checkHCLKeys(o.Val, valid); err != nil {
|
|
return err
|
|
}
|
|
|
|
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
|
WeaklyTypedInput: true,
|
|
Result: result,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return dec.Decode(m)
|
|
}
|
|
|
|
func parseMigrate(result **api.MigrateStrategy, list *ast.ObjectList) error {
|
|
list = list.Elem()
|
|
if len(list.Items) > 1 {
|
|
return fmt.Errorf("only one 'migrate' block allowed")
|
|
}
|
|
|
|
// Get our resource object
|
|
o := list.Items[0]
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"max_parallel",
|
|
"health_check",
|
|
"min_healthy_time",
|
|
"healthy_deadline",
|
|
}
|
|
if err := checkHCLKeys(o.Val, valid); err != nil {
|
|
return err
|
|
}
|
|
|
|
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
|
WeaklyTypedInput: true,
|
|
Result: result,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return dec.Decode(m)
|
|
}
|
|
|
|
func parseVault(result *api.Vault, list *ast.ObjectList) error {
|
|
list = list.Elem()
|
|
if len(list.Items) == 0 {
|
|
return nil
|
|
}
|
|
if len(list.Items) > 1 {
|
|
return fmt.Errorf("only one 'vault' block allowed per task")
|
|
}
|
|
|
|
// Get our resource object
|
|
o := list.Items[0]
|
|
|
|
// We need this later
|
|
var listVal *ast.ObjectList
|
|
if ot, ok := o.Val.(*ast.ObjectType); ok {
|
|
listVal = ot.List
|
|
} else {
|
|
return fmt.Errorf("vault: should be an object")
|
|
}
|
|
|
|
// Check for invalid keys
|
|
valid := []string{
|
|
"namespace",
|
|
"policies",
|
|
"env",
|
|
"disable_file",
|
|
"change_mode",
|
|
"change_signal",
|
|
}
|
|
if err := checkHCLKeys(listVal, valid); err != nil {
|
|
return multierror.Prefix(err, "vault ->")
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := mapstructure.WeakDecode(m, result); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|