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 }