open-nomad/jobspec2/hcl_conversions.go

506 lines
15 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package jobspec2
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
var hclDecoder *gohcl.Decoder
func init() {
hclDecoder = newHCLDecoder()
hclDecoder.RegisterBlockDecoder(reflect.TypeOf(api.TaskGroup{}), decodeTaskGroup)
hclDecoder.RegisterBlockDecoder(reflect.TypeOf(api.Task{}), decodeTask)
}
func newHCLDecoder() *gohcl.Decoder {
decoder := &gohcl.Decoder{}
// time conversion
d := time.Duration(0)
decoder.RegisterExpressionDecoder(reflect.TypeOf(d), decodeDuration)
decoder.RegisterExpressionDecoder(reflect.TypeOf(&d), decodeDuration)
// custom nomad types
decoder.RegisterBlockDecoder(reflect.TypeOf(api.Affinity{}), decodeAffinity)
decoder.RegisterBlockDecoder(reflect.TypeOf(api.Constraint{}), decodeConstraint)
return decoder
}
func decodeDuration(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
srcVal, diags := expr.Value(ctx)
if srcVal.Type() == cty.String {
dur, err := time.ParseDuration(srcVal.AsString())
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsuitable value type",
Detail: fmt.Sprintf("Unsuitable duration value: %s", err.Error()),
Subject: expr.StartRange().Ptr(),
Context: expr.Range().Ptr(),
})
return diags
}
srcVal = cty.NumberIntVal(int64(dur))
}
if srcVal.Type() != cty.Number {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsuitable value type",
Detail: fmt.Sprintf("Unsuitable value: expected a string but found %s", srcVal.Type()),
Subject: expr.StartRange().Ptr(),
Context: expr.Range().Ptr(),
})
return diags
}
err := gocty.FromCtyValue(srcVal, val)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unsuitable value type",
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
Subject: expr.StartRange().Ptr(),
Context: expr.Range().Ptr(),
})
}
return diags
}
var affinitySpec = hcldec.ObjectSpec{
"attribute": &hcldec.AttrSpec{Name: "attribute", Type: cty.String, Required: false},
"value": &hcldec.AttrSpec{Name: "value", Type: cty.String, Required: false},
"operator": &hcldec.AttrSpec{Name: "operator", Type: cty.String, Required: false},
"weight": &hcldec.AttrSpec{Name: "weight", Type: cty.Number, Required: false},
api.ConstraintVersion: &hcldec.AttrSpec{Name: api.ConstraintVersion, Type: cty.String, Required: false},
api.ConstraintSemver: &hcldec.AttrSpec{Name: api.ConstraintSemver, Type: cty.String, Required: false},
api.ConstraintRegex: &hcldec.AttrSpec{Name: api.ConstraintRegex, Type: cty.String, Required: false},
api.ConstraintSetContains: &hcldec.AttrSpec{Name: api.ConstraintSetContains, Type: cty.String, Required: false},
api.ConstraintSetContainsAll: &hcldec.AttrSpec{Name: api.ConstraintSetContainsAll, Type: cty.String, Required: false},
api.ConstraintSetContainsAny: &hcldec.AttrSpec{Name: api.ConstraintSetContainsAny, Type: cty.String, Required: false},
}
func decodeAffinity(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
a := val.(*api.Affinity)
v, diags := hcldec.Decode(body, affinitySpec, ctx)
if len(diags) != 0 {
return diags
}
attr := func(attr string) string {
a := v.GetAttr(attr)
if a.IsNull() {
return ""
}
return a.AsString()
}
a.LTarget = attr("attribute")
a.RTarget = attr("value")
a.Operand = attr("operator")
weight := v.GetAttr("weight")
if !weight.IsNull() {
w, _ := weight.AsBigFloat().Int64()
a.Weight = pointer.Of(int8(w))
}
// If "version" is provided, set the operand
// to "version" and the value to the "RTarget"
if affinity := attr(api.ConstraintVersion); affinity != "" {
a.Operand = api.ConstraintVersion
a.RTarget = affinity
}
// If "semver" is provided, set the operand
// to "semver" and the value to the "RTarget"
if affinity := attr(api.ConstraintSemver); affinity != "" {
a.Operand = api.ConstraintSemver
a.RTarget = affinity
}
// If "regexp" is provided, set the operand
// to "regexp" and the value to the "RTarget"
if affinity := attr(api.ConstraintRegex); affinity != "" {
a.Operand = api.ConstraintRegex
a.RTarget = affinity
}
// If "set_contains_any" is provided, set the operand
// to "set_contains_any" and the value to the "RTarget"
if affinity := attr(api.ConstraintSetContainsAny); affinity != "" {
a.Operand = api.ConstraintSetContainsAny
a.RTarget = affinity
}
// If "set_contains_all" is provided, set the operand
// to "set_contains_all" and the value to the "RTarget"
if affinity := attr(api.ConstraintSetContainsAll); affinity != "" {
a.Operand = api.ConstraintSetContainsAll
a.RTarget = affinity
}
// set_contains is a synonym of set_contains_all
if affinity := attr(api.ConstraintSetContains); affinity != "" {
a.Operand = api.ConstraintSetContains
a.RTarget = affinity
}
if a.Operand == "" {
a.Operand = "="
}
return diags
}
var constraintSpec = hcldec.ObjectSpec{
"attribute": &hcldec.AttrSpec{Name: "attribute", Type: cty.String, Required: false},
"value": &hcldec.AttrSpec{Name: "value", Type: cty.String, Required: false},
"operator": &hcldec.AttrSpec{Name: "operator", Type: cty.String, Required: false},
api.ConstraintDistinctProperty: &hcldec.AttrSpec{Name: api.ConstraintDistinctProperty, Type: cty.String, Required: false},
api.ConstraintDistinctHosts: &hcldec.AttrSpec{Name: api.ConstraintDistinctHosts, Type: cty.Bool, Required: false},
api.ConstraintRegex: &hcldec.AttrSpec{Name: api.ConstraintRegex, Type: cty.String, Required: false},
api.ConstraintVersion: &hcldec.AttrSpec{Name: api.ConstraintVersion, Type: cty.String, Required: false},
api.ConstraintSemver: &hcldec.AttrSpec{Name: api.ConstraintSemver, Type: cty.String, Required: false},
api.ConstraintSetContains: &hcldec.AttrSpec{Name: api.ConstraintSetContains, Type: cty.String, Required: false},
api.ConstraintSetContainsAll: &hcldec.AttrSpec{Name: api.ConstraintSetContainsAll, Type: cty.String, Required: false},
api.ConstraintSetContainsAny: &hcldec.AttrSpec{Name: api.ConstraintSetContainsAny, Type: cty.String, Required: false},
api.ConstraintAttributeIsSet: &hcldec.AttrSpec{Name: api.ConstraintAttributeIsSet, Type: cty.String, Required: false},
api.ConstraintAttributeIsNotSet: &hcldec.AttrSpec{Name: api.ConstraintAttributeIsNotSet, Type: cty.String, Required: false},
}
func decodeConstraint(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
c := val.(*api.Constraint)
v, diags := hcldec.Decode(body, constraintSpec, ctx)
if len(diags) != 0 {
return diags
}
attr := func(attr string) string {
a := v.GetAttr(attr)
if a.IsNull() {
return ""
}
return a.AsString()
}
c.LTarget = attr("attribute")
c.RTarget = attr("value")
c.Operand = attr("operator")
// If "version" is provided, set the operand
// to "version" and the value to the "RTarget"
if constraint := attr(api.ConstraintVersion); constraint != "" {
c.Operand = api.ConstraintVersion
c.RTarget = constraint
}
// If "semver" is provided, set the operand
// to "semver" and the value to the "RTarget"
if constraint := attr(api.ConstraintSemver); constraint != "" {
c.Operand = api.ConstraintSemver
c.RTarget = constraint
}
// If "regexp" is provided, set the operand
// to "regexp" and the value to the "RTarget"
if constraint := attr(api.ConstraintRegex); constraint != "" {
c.Operand = api.ConstraintRegex
c.RTarget = constraint
}
// If "set_contains" is provided, set the operand
// to "set_contains" and the value to the "RTarget"
if constraint := attr(api.ConstraintSetContains); constraint != "" {
c.Operand = api.ConstraintSetContains
c.RTarget = constraint
}
// The shortcut form of the distinct_hosts constraint is a cty.Bool
// so it can not use the `attr` func defined earlier
if d := v.GetAttr(api.ConstraintDistinctHosts); !d.IsNull() {
c.Operand = api.ConstraintDistinctHosts
c.RTarget = fmt.Sprint(d.True())
}
if property := attr(api.ConstraintDistinctProperty); property != "" {
c.Operand = api.ConstraintDistinctProperty
c.LTarget = property
}
if c.Operand == "" {
c.Operand = "="
}
return diags
}
func decodeTaskGroup(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
tg := val.(*api.TaskGroup)
var diags hcl.Diagnostics
metaAttr, body, moreDiags := decodeAsAttribute(body, ctx, "meta")
diags = append(diags, moreDiags...)
tgExtra := struct {
Vault *api.Vault `hcl:"vault,block"`
}{}
extra, _ := gohcl.ImpliedBodySchema(tgExtra)
content, tgBody, moreDiags := body.PartialContent(extra)
diags = append(diags, moreDiags...)
if len(diags) != 0 {
return diags
}
for _, b := range content.Blocks {
if b.Type == "vault" {
v := &api.Vault{}
diags = append(diags, hclDecoder.DecodeBody(b.Body, ctx, v)...)
tgExtra.Vault = v
}
}
d := newHCLDecoder()
d.RegisterBlockDecoder(reflect.TypeOf(api.Task{}), decodeTask)
diags = d.DecodeBody(tgBody, ctx, tg)
if metaAttr != nil {
tg.Meta = metaAttr
}
if tgExtra.Vault != nil {
for _, t := range tg.Tasks {
if t.Vault == nil {
t.Vault = tgExtra.Vault
}
}
}
if tg.Scaling != nil {
if tg.Scaling.Type == "" {
tg.Scaling.Type = "horizontal"
}
diags = append(diags, validateGroupScalingPolicy(tg.Scaling, tgBody)...)
}
return diags
}
func decodeTask(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
// special case scaling policy
t := val.(*api.Task)
var diags hcl.Diagnostics
// special case env and meta
envAttr, body, moreDiags := decodeAsAttribute(body, ctx, "env")
diags = append(diags, moreDiags...)
metaAttr, body, moreDiags := decodeAsAttribute(body, ctx, "meta")
diags = append(diags, moreDiags...)
b, remain, moreDiags := body.PartialContent(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: "scaling", LabelNames: []string{"name"}},
},
})
diags = append(diags, moreDiags...)
diags = append(diags, decodeTaskScalingPolicies(b.Blocks, ctx, t)...)
decoder := newHCLDecoder()
diags = append(diags, decoder.DecodeBody(remain, ctx, val)...)
if envAttr != nil {
t.Env = envAttr
}
if metaAttr != nil {
t.Meta = metaAttr
}
return diags
}
// decodeAsAttribute decodes the named field as an attribute assignment if found.
//
// Nomad jobs contain attributes (e.g. `env`, `meta`) that are meant to contain arbitrary
// keys. HCLv1 allowed both block syntax (the preferred and documented one) as well as attribute
// assignment syntax:
//
// ```hcl
// # block assignment
//
// env {
// ENV = "production"
// }
//
// # as attribute
// env = { ENV: "production" }
// ```
//
// HCLv2 block syntax, though, restricts valid input and doesn't allow dots or invalid identifiers
// as block attribute keys.
// Thus, we support both syntax to unrestrict users.
//
// This function attempts to read the named field, as an attribute, and returns
// found map, the remaining body and diagnostics. If the named field is found
// with block syntax, it returns a nil map, and caller falls back to reading
// with block syntax.
func decodeAsAttribute(body hcl.Body, ctx *hcl.EvalContext, name string) (map[string]string, hcl.Body, hcl.Diagnostics) {
b, remain, diags := body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: name, Required: false},
},
})
if diags.HasErrors() || b.Attributes[name] == nil {
// ignoring errors, to avoid duplicate errors. True errors will
// reported in the fallback path
return nil, body, nil
}
attr := b.Attributes[name]
if attr != nil {
// check if there is another block
bb, _, _ := remain.PartialContent(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{{Type: name}},
})
if len(bb.Blocks) != 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate %v block", name),
Detail: fmt.Sprintf("%v may not be defined more than once. Another definition is defined at %s.",
name, attr.Range.String()),
Subject: &bb.Blocks[0].DefRange,
})
return nil, remain, diags
}
}
envExpr := attr.Expr
result := map[string]string{}
diags = append(diags, hclDecoder.DecodeExpression(envExpr, ctx, &result)...)
return result, remain, diags
}
func decodeTaskScalingPolicies(blocks hcl.Blocks, ctx *hcl.EvalContext, task *api.Task) hcl.Diagnostics {
if len(blocks) == 0 {
return nil
}
var diags hcl.Diagnostics
seen := map[string]*hcl.Block{}
for _, b := range blocks {
label := strings.ToLower(b.Labels[0])
var policyType string
switch label {
case "cpu":
policyType = "vertical_cpu"
case "mem":
policyType = "vertical_mem"
default:
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid scaling policy name",
Detail: `scaling policy name must be "cpu" or "mem"`,
Subject: &b.LabelRanges[0],
})
continue
}
if prev, ok := seen[label]; ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate scaling %q block", label),
Detail: fmt.Sprintf(
"Only one scaling %s block is allowed. Another was defined at %s.",
label, prev.DefRange.String(),
),
Subject: &b.DefRange,
})
continue
}
seen[label] = b
var p api.ScalingPolicy
diags = append(diags, hclDecoder.DecodeBody(b.Body, ctx, &p)...)
if p.Type == "" {
p.Type = policyType
} else if p.Type != policyType {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid scaling policy type",
Detail: fmt.Sprintf(
"Invalid policy type, expected %q but found %q",
p.Type, policyType),
Subject: &b.DefRange,
})
continue
}
task.ScalingPolicies = append(task.ScalingPolicies, &p)
}
return diags
}
func validateGroupScalingPolicy(p *api.ScalingPolicy, body hcl.Body) hcl.Diagnostics {
// fast path: do nothing
if p.Max != nil && p.Type == "horizontal" {
return nil
}
content, _, diags := body.PartialContent(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{{Type: "scaling"}},
})
if len(content.Blocks) == 0 {
// unexpected, given that we have a scaling policy
return diags
}
pc, _, diags := content.Blocks[0].Body.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{Name: "max", Required: true},
{Name: "type", Required: false},
},
})
if p.Type != "horizontal" {
if attr, ok := pc.Attributes["type"]; ok {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid group scaling type",
Detail: fmt.Sprintf(
"task group scaling policy had invalid type: %q",
p.Type),
Subject: attr.Expr.Range().Ptr(),
})
}
}
return diags
}