github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/jobspec2/hcl_conversions.go (about)

     1  package jobspec2
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/gohcl"
    11  	"github.com/hashicorp/hcl/v2/hcldec"
    12  	"github.com/hashicorp/nomad/api"
    13  	"github.com/zclconf/go-cty/cty"
    14  	"github.com/zclconf/go-cty/cty/gocty"
    15  )
    16  
    17  var hclDecoder *gohcl.Decoder
    18  
    19  func init() {
    20  	hclDecoder = newHCLDecoder()
    21  	hclDecoder.RegisterBlockDecoder(reflect.TypeOf(api.TaskGroup{}), decodeTaskGroup)
    22  	hclDecoder.RegisterBlockDecoder(reflect.TypeOf(api.Task{}), decodeTask)
    23  }
    24  
    25  func newHCLDecoder() *gohcl.Decoder {
    26  	decoder := &gohcl.Decoder{}
    27  
    28  	// time conversion
    29  	d := time.Duration(0)
    30  	decoder.RegisterExpressionDecoder(reflect.TypeOf(d), decodeDuration)
    31  	decoder.RegisterExpressionDecoder(reflect.TypeOf(&d), decodeDuration)
    32  
    33  	// custom nomad types
    34  	decoder.RegisterBlockDecoder(reflect.TypeOf(api.Affinity{}), decodeAffinity)
    35  	decoder.RegisterBlockDecoder(reflect.TypeOf(api.Constraint{}), decodeConstraint)
    36  
    37  	return decoder
    38  }
    39  
    40  func decodeDuration(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
    41  	srcVal, diags := expr.Value(ctx)
    42  
    43  	if srcVal.Type() == cty.String {
    44  		dur, err := time.ParseDuration(srcVal.AsString())
    45  		if err != nil {
    46  			diags = append(diags, &hcl.Diagnostic{
    47  				Severity: hcl.DiagError,
    48  				Summary:  "Unsuitable value type",
    49  				Detail:   fmt.Sprintf("Unsuitable duration value: %s", err.Error()),
    50  				Subject:  expr.StartRange().Ptr(),
    51  				Context:  expr.Range().Ptr(),
    52  			})
    53  			return diags
    54  		}
    55  
    56  		srcVal = cty.NumberIntVal(int64(dur))
    57  	}
    58  
    59  	if srcVal.Type() != cty.Number {
    60  		diags = append(diags, &hcl.Diagnostic{
    61  			Severity: hcl.DiagError,
    62  			Summary:  "Unsuitable value type",
    63  			Detail:   fmt.Sprintf("Unsuitable value: expected a string but found %s", srcVal.Type()),
    64  			Subject:  expr.StartRange().Ptr(),
    65  			Context:  expr.Range().Ptr(),
    66  		})
    67  		return diags
    68  
    69  	}
    70  
    71  	err := gocty.FromCtyValue(srcVal, val)
    72  	if err != nil {
    73  		diags = append(diags, &hcl.Diagnostic{
    74  			Severity: hcl.DiagError,
    75  			Summary:  "Unsuitable value type",
    76  			Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
    77  			Subject:  expr.StartRange().Ptr(),
    78  			Context:  expr.Range().Ptr(),
    79  		})
    80  	}
    81  
    82  	return diags
    83  }
    84  
    85  var affinitySpec = hcldec.ObjectSpec{
    86  	"attribute": &hcldec.AttrSpec{Name: "attribute", Type: cty.String, Required: false},
    87  	"value":     &hcldec.AttrSpec{Name: "value", Type: cty.String, Required: false},
    88  	"operator":  &hcldec.AttrSpec{Name: "operator", Type: cty.String, Required: false},
    89  	"weight":    &hcldec.AttrSpec{Name: "weight", Type: cty.Number, Required: false},
    90  
    91  	api.ConstraintVersion:        &hcldec.AttrSpec{Name: api.ConstraintVersion, Type: cty.String, Required: false},
    92  	api.ConstraintSemver:         &hcldec.AttrSpec{Name: api.ConstraintSemver, Type: cty.String, Required: false},
    93  	api.ConstraintRegex:          &hcldec.AttrSpec{Name: api.ConstraintRegex, Type: cty.String, Required: false},
    94  	api.ConstraintSetContains:    &hcldec.AttrSpec{Name: api.ConstraintSetContains, Type: cty.String, Required: false},
    95  	api.ConstraintSetContainsAll: &hcldec.AttrSpec{Name: api.ConstraintSetContainsAll, Type: cty.String, Required: false},
    96  	api.ConstraintSetContainsAny: &hcldec.AttrSpec{Name: api.ConstraintSetContainsAny, Type: cty.String, Required: false},
    97  }
    98  
    99  func decodeAffinity(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
   100  	a := val.(*api.Affinity)
   101  	v, diags := hcldec.Decode(body, affinitySpec, ctx)
   102  	if len(diags) != 0 {
   103  		return diags
   104  	}
   105  
   106  	attr := func(attr string) string {
   107  		a := v.GetAttr(attr)
   108  		if a.IsNull() {
   109  			return ""
   110  		}
   111  		return a.AsString()
   112  	}
   113  	a.LTarget = attr("attribute")
   114  	a.RTarget = attr("value")
   115  	a.Operand = attr("operator")
   116  	weight := v.GetAttr("weight")
   117  	if !weight.IsNull() {
   118  		w, _ := weight.AsBigFloat().Int64()
   119  		a.Weight = int8ToPtr(int8(w))
   120  	}
   121  
   122  	// If "version" is provided, set the operand
   123  	// to "version" and the value to the "RTarget"
   124  	if affinity := attr(api.ConstraintVersion); affinity != "" {
   125  		a.Operand = api.ConstraintVersion
   126  		a.RTarget = affinity
   127  	}
   128  
   129  	// If "semver" is provided, set the operand
   130  	// to "semver" and the value to the "RTarget"
   131  	if affinity := attr(api.ConstraintSemver); affinity != "" {
   132  		a.Operand = api.ConstraintSemver
   133  		a.RTarget = affinity
   134  	}
   135  
   136  	// If "regexp" is provided, set the operand
   137  	// to "regexp" and the value to the "RTarget"
   138  	if affinity := attr(api.ConstraintRegex); affinity != "" {
   139  		a.Operand = api.ConstraintRegex
   140  		a.RTarget = affinity
   141  	}
   142  
   143  	// If "set_contains_any" is provided, set the operand
   144  	// to "set_contains_any" and the value to the "RTarget"
   145  	if affinity := attr(api.ConstraintSetContainsAny); affinity != "" {
   146  		a.Operand = api.ConstraintSetContainsAny
   147  		a.RTarget = affinity
   148  	}
   149  
   150  	// If "set_contains_all" is provided, set the operand
   151  	// to "set_contains_all" and the value to the "RTarget"
   152  	if affinity := attr(api.ConstraintSetContainsAll); affinity != "" {
   153  		a.Operand = api.ConstraintSetContainsAll
   154  		a.RTarget = affinity
   155  	}
   156  
   157  	// set_contains is a synonym of set_contains_all
   158  	if affinity := attr(api.ConstraintSetContains); affinity != "" {
   159  		a.Operand = api.ConstraintSetContains
   160  		a.RTarget = affinity
   161  	}
   162  
   163  	if a.Operand == "" {
   164  		a.Operand = "="
   165  	}
   166  	return diags
   167  }
   168  
   169  var constraintSpec = hcldec.ObjectSpec{
   170  	"attribute": &hcldec.AttrSpec{Name: "attribute", Type: cty.String, Required: false},
   171  	"value":     &hcldec.AttrSpec{Name: "value", Type: cty.String, Required: false},
   172  	"operator":  &hcldec.AttrSpec{Name: "operator", Type: cty.String, Required: false},
   173  
   174  	api.ConstraintDistinctProperty:  &hcldec.AttrSpec{Name: api.ConstraintDistinctProperty, Type: cty.String, Required: false},
   175  	api.ConstraintDistinctHosts:     &hcldec.AttrSpec{Name: api.ConstraintDistinctHosts, Type: cty.Bool, Required: false},
   176  	api.ConstraintRegex:             &hcldec.AttrSpec{Name: api.ConstraintRegex, Type: cty.String, Required: false},
   177  	api.ConstraintVersion:           &hcldec.AttrSpec{Name: api.ConstraintVersion, Type: cty.String, Required: false},
   178  	api.ConstraintSemver:            &hcldec.AttrSpec{Name: api.ConstraintSemver, Type: cty.String, Required: false},
   179  	api.ConstraintSetContains:       &hcldec.AttrSpec{Name: api.ConstraintSetContains, Type: cty.String, Required: false},
   180  	api.ConstraintSetContainsAll:    &hcldec.AttrSpec{Name: api.ConstraintSetContainsAll, Type: cty.String, Required: false},
   181  	api.ConstraintSetContainsAny:    &hcldec.AttrSpec{Name: api.ConstraintSetContainsAny, Type: cty.String, Required: false},
   182  	api.ConstraintAttributeIsSet:    &hcldec.AttrSpec{Name: api.ConstraintAttributeIsSet, Type: cty.String, Required: false},
   183  	api.ConstraintAttributeIsNotSet: &hcldec.AttrSpec{Name: api.ConstraintAttributeIsNotSet, Type: cty.String, Required: false},
   184  }
   185  
   186  func decodeConstraint(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
   187  	c := val.(*api.Constraint)
   188  
   189  	v, diags := hcldec.Decode(body, constraintSpec, ctx)
   190  	if len(diags) != 0 {
   191  		return diags
   192  	}
   193  
   194  	attr := func(attr string) string {
   195  		a := v.GetAttr(attr)
   196  		if a.IsNull() {
   197  			return ""
   198  		}
   199  		return a.AsString()
   200  	}
   201  
   202  	c.LTarget = attr("attribute")
   203  	c.RTarget = attr("value")
   204  	c.Operand = attr("operator")
   205  
   206  	// If "version" is provided, set the operand
   207  	// to "version" and the value to the "RTarget"
   208  	if constraint := attr(api.ConstraintVersion); constraint != "" {
   209  		c.Operand = api.ConstraintVersion
   210  		c.RTarget = constraint
   211  	}
   212  
   213  	// If "semver" is provided, set the operand
   214  	// to "semver" and the value to the "RTarget"
   215  	if constraint := attr(api.ConstraintSemver); constraint != "" {
   216  		c.Operand = api.ConstraintSemver
   217  		c.RTarget = constraint
   218  	}
   219  
   220  	// If "regexp" is provided, set the operand
   221  	// to "regexp" and the value to the "RTarget"
   222  	if constraint := attr(api.ConstraintRegex); constraint != "" {
   223  		c.Operand = api.ConstraintRegex
   224  		c.RTarget = constraint
   225  	}
   226  
   227  	// If "set_contains" is provided, set the operand
   228  	// to "set_contains" and the value to the "RTarget"
   229  	if constraint := attr(api.ConstraintSetContains); constraint != "" {
   230  		c.Operand = api.ConstraintSetContains
   231  		c.RTarget = constraint
   232  	}
   233  
   234  	if d := v.GetAttr(api.ConstraintDistinctHosts); !d.IsNull() && d.True() {
   235  		c.Operand = api.ConstraintDistinctHosts
   236  	}
   237  
   238  	if property := attr(api.ConstraintDistinctProperty); property != "" {
   239  		c.Operand = api.ConstraintDistinctProperty
   240  		c.LTarget = property
   241  	}
   242  
   243  	if c.Operand == "" {
   244  		c.Operand = "="
   245  	}
   246  	return diags
   247  }
   248  
   249  func decodeTaskGroup(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
   250  	tg := val.(*api.TaskGroup)
   251  
   252  	var diags hcl.Diagnostics
   253  
   254  	metaAttr, body, moreDiags := decodeAsAttribute(body, ctx, "meta")
   255  	diags = append(diags, moreDiags...)
   256  
   257  	tgExtra := struct {
   258  		Vault *api.Vault `hcl:"vault,block"`
   259  	}{}
   260  
   261  	extra, _ := gohcl.ImpliedBodySchema(tgExtra)
   262  	content, tgBody, moreDiags := body.PartialContent(extra)
   263  	diags = append(diags, moreDiags...)
   264  	if len(diags) != 0 {
   265  		return diags
   266  	}
   267  
   268  	for _, b := range content.Blocks {
   269  		if b.Type == "vault" {
   270  			v := &api.Vault{}
   271  			diags = append(diags, hclDecoder.DecodeBody(b.Body, ctx, v)...)
   272  			tgExtra.Vault = v
   273  		}
   274  	}
   275  
   276  	d := newHCLDecoder()
   277  	d.RegisterBlockDecoder(reflect.TypeOf(api.Task{}), decodeTask)
   278  	diags = d.DecodeBody(tgBody, ctx, tg)
   279  
   280  	if metaAttr != nil {
   281  		tg.Meta = metaAttr
   282  	}
   283  
   284  	if tgExtra.Vault != nil {
   285  		for _, t := range tg.Tasks {
   286  			if t.Vault == nil {
   287  				t.Vault = tgExtra.Vault
   288  			}
   289  		}
   290  	}
   291  
   292  	if tg.Scaling != nil {
   293  		if tg.Scaling.Type == "" {
   294  			tg.Scaling.Type = "horizontal"
   295  		}
   296  		diags = append(diags, validateGroupScalingPolicy(tg.Scaling, tgBody)...)
   297  	}
   298  	return diags
   299  
   300  }
   301  
   302  func decodeTask(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
   303  	// special case scaling policy
   304  	t := val.(*api.Task)
   305  
   306  	var diags hcl.Diagnostics
   307  
   308  	// special case env and meta
   309  	envAttr, body, moreDiags := decodeAsAttribute(body, ctx, "env")
   310  	diags = append(diags, moreDiags...)
   311  	metaAttr, body, moreDiags := decodeAsAttribute(body, ctx, "meta")
   312  	diags = append(diags, moreDiags...)
   313  
   314  	b, remain, moreDiags := body.PartialContent(&hcl.BodySchema{
   315  		Blocks: []hcl.BlockHeaderSchema{
   316  			{Type: "scaling", LabelNames: []string{"name"}},
   317  		},
   318  	})
   319  
   320  	diags = append(diags, moreDiags...)
   321  	diags = append(diags, decodeTaskScalingPolicies(b.Blocks, ctx, t)...)
   322  
   323  	decoder := newHCLDecoder()
   324  	diags = append(diags, decoder.DecodeBody(remain, ctx, val)...)
   325  
   326  	if envAttr != nil {
   327  		t.Env = envAttr
   328  	}
   329  	if metaAttr != nil {
   330  		t.Meta = metaAttr
   331  	}
   332  
   333  	return diags
   334  }
   335  
   336  // decodeAsAttribute decodes the named field as an attribute assignment if found.
   337  //
   338  // Nomad jobs contain attributes (e.g. `env`, `meta`) that are meant to contain arbitrary
   339  // keys. HCLv1 allowed both block syntax (the preferred and documented one) as well as attribute
   340  // assignment syntax:
   341  //
   342  // ```hcl
   343  // # block assignment
   344  // env {
   345  //   ENV = "production"
   346  // }
   347  //
   348  // # as attribute
   349  // env = { ENV: "production" }
   350  // ```
   351  //
   352  // HCLv2 block syntax, though, restricts valid input and doesn't allow dots or invalid identifiers
   353  // as block attribute keys.
   354  // Thus, we support both syntax to unrestrict users.
   355  //
   356  // This function attempts to read the named field, as an attribute, and returns
   357  // found map, the remaining body and diagnostics. If the named field is found
   358  // with block syntax, it returns a nil map, and caller falls back to reading
   359  // with block syntax.
   360  //
   361  func decodeAsAttribute(body hcl.Body, ctx *hcl.EvalContext, name string) (map[string]string, hcl.Body, hcl.Diagnostics) {
   362  	b, remain, diags := body.PartialContent(&hcl.BodySchema{
   363  		Attributes: []hcl.AttributeSchema{
   364  			{Name: name, Required: false},
   365  		},
   366  	})
   367  
   368  	if diags.HasErrors() || b.Attributes[name] == nil {
   369  		// ignoring errors, to avoid duplicate errors. True errors will
   370  		// reported in the fallback path
   371  		return nil, body, nil
   372  	}
   373  
   374  	attr := b.Attributes[name]
   375  
   376  	if attr != nil {
   377  		// check if there is another block
   378  		bb, _, _ := remain.PartialContent(&hcl.BodySchema{
   379  			Blocks: []hcl.BlockHeaderSchema{{Type: name}},
   380  		})
   381  		if len(bb.Blocks) != 0 {
   382  			diags = diags.Append(&hcl.Diagnostic{
   383  				Severity: hcl.DiagError,
   384  				Summary:  fmt.Sprintf("Duplicate %v block", name),
   385  				Detail: fmt.Sprintf("%v may not be defined more than once. Another definition is defined at %s.",
   386  					name, attr.Range.String()),
   387  				Subject: &bb.Blocks[0].DefRange,
   388  			})
   389  			return nil, remain, diags
   390  		}
   391  	}
   392  
   393  	envExpr := attr.Expr
   394  
   395  	result := map[string]string{}
   396  	diags = append(diags, hclDecoder.DecodeExpression(envExpr, ctx, &result)...)
   397  
   398  	return result, remain, diags
   399  }
   400  
   401  func decodeTaskScalingPolicies(blocks hcl.Blocks, ctx *hcl.EvalContext, task *api.Task) hcl.Diagnostics {
   402  	if len(blocks) == 0 {
   403  		return nil
   404  	}
   405  
   406  	var diags hcl.Diagnostics
   407  	seen := map[string]*hcl.Block{}
   408  	for _, b := range blocks {
   409  		label := strings.ToLower(b.Labels[0])
   410  		var policyType string
   411  		switch label {
   412  		case "cpu":
   413  			policyType = "vertical_cpu"
   414  		case "mem":
   415  			policyType = "vertical_mem"
   416  		default:
   417  			diags = append(diags, &hcl.Diagnostic{
   418  				Severity: hcl.DiagError,
   419  				Summary:  "Invalid scaling policy name",
   420  				Detail:   `scaling policy name must be "cpu" or "mem"`,
   421  				Subject:  &b.LabelRanges[0],
   422  			})
   423  			continue
   424  		}
   425  
   426  		if prev, ok := seen[label]; ok {
   427  			diags = append(diags, &hcl.Diagnostic{
   428  				Severity: hcl.DiagError,
   429  				Summary:  fmt.Sprintf("Duplicate scaling %q block", label),
   430  				Detail: fmt.Sprintf(
   431  					"Only one scaling %s block is allowed. Another was defined at %s.",
   432  					label, prev.DefRange.String(),
   433  				),
   434  				Subject: &b.DefRange,
   435  			})
   436  			continue
   437  		}
   438  		seen[label] = b
   439  
   440  		var p api.ScalingPolicy
   441  		diags = append(diags, hclDecoder.DecodeBody(b.Body, ctx, &p)...)
   442  
   443  		if p.Type == "" {
   444  			p.Type = policyType
   445  		} else if p.Type != policyType {
   446  			diags = append(diags, &hcl.Diagnostic{
   447  				Severity: hcl.DiagError,
   448  				Summary:  "Invalid scaling policy type",
   449  				Detail: fmt.Sprintf(
   450  					"Invalid policy type, expected %q but found %q",
   451  					p.Type, policyType),
   452  				Subject: &b.DefRange,
   453  			})
   454  			continue
   455  		}
   456  
   457  		task.ScalingPolicies = append(task.ScalingPolicies, &p)
   458  	}
   459  
   460  	return diags
   461  }
   462  
   463  func validateGroupScalingPolicy(p *api.ScalingPolicy, body hcl.Body) hcl.Diagnostics {
   464  	// fast path: do nothing
   465  	if p.Max != nil && p.Type == "horizontal" {
   466  		return nil
   467  	}
   468  
   469  	content, _, diags := body.PartialContent(&hcl.BodySchema{
   470  		Blocks: []hcl.BlockHeaderSchema{{Type: "scaling"}},
   471  	})
   472  
   473  	if len(content.Blocks) == 0 {
   474  		// unexpected, given that we have a scaling policy
   475  		return diags
   476  	}
   477  
   478  	pc, _, diags := content.Blocks[0].Body.PartialContent(&hcl.BodySchema{
   479  		Attributes: []hcl.AttributeSchema{
   480  			{Name: "max", Required: true},
   481  			{Name: "type", Required: false},
   482  		},
   483  	})
   484  
   485  	if p.Type != "horizontal" {
   486  		if attr, ok := pc.Attributes["type"]; ok {
   487  			diags = append(diags, &hcl.Diagnostic{
   488  				Severity: hcl.DiagError,
   489  				Summary:  "Invalid group scaling type",
   490  				Detail: fmt.Sprintf(
   491  					"task group scaling policy had invalid type: %q",
   492  					p.Type),
   493  				Subject: attr.Expr.Range().Ptr(),
   494  			})
   495  		}
   496  	}
   497  	return diags
   498  }