// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package jobspec2 import ( "fmt" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/dynblock" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/jobspec2/hclutil" "github.com/zclconf/go-cty/cty" ) const ( variablesLabel = "variables" variableLabel = "variable" localsLabel = "locals" vaultLabel = "vault" taskLabel = "task" inputVariablesAccessor = "var" localsAccessor = "local" ) type jobConfig struct { JobID string `hcl:",label"` Job *api.Job ParseConfig *ParseConfig Vault *api.Vault `hcl:"vault,block"` Tasks []*api.Task `hcl:"task,block"` InputVariables Variables LocalVariables Variables LocalBlocks []*LocalBlock } func newJobConfig(parseConfig *ParseConfig) *jobConfig { return &jobConfig{ ParseConfig: parseConfig, InputVariables: Variables{}, LocalVariables: Variables{}, } } var jobConfigSchema = &hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ {Type: variablesLabel}, {Type: variableLabel, LabelNames: []string{"name"}}, {Type: localsLabel}, {Type: "job", LabelNames: []string{"name"}}, }, } func (c *jobConfig) decodeBody(body hcl.Body) hcl.Diagnostics { content, diags := body.Content(jobConfigSchema) if len(diags) != 0 { return diags } diags = append(diags, c.decodeInputVariables(content)...) diags = append(diags, c.parseLocalVariables(content)...) diags = append(diags, c.collectInputVariableValues(c.ParseConfig.Envs, c.ParseConfig.parsedVarFiles, toVars(c.ParseConfig.ArgVars))...) _, moreDiags := c.InputVariables.Values() diags = append(diags, moreDiags...) _, moreDiags = c.LocalVariables.Values() diags = append(diags, moreDiags...) diags = append(diags, c.evaluateLocalVariables(c.LocalBlocks)...) // Errors at this point are likely syntax errors which can result in // invalid state when we try to decode the rest of the job. If we continue // we may panic and that will obscure the error, so return early so the // user can be told how to fix their jobspec. if diags.HasErrors() { return diags } nctx := c.EvalContext() diags = append(diags, c.decodeJob(content, nctx)...) return diags } // decodeInputVariables looks in the found blocks for 'variables' and // 'variable' blocks. It should be called firsthand so that other blocks can // use the variables. func (c *jobConfig) decodeInputVariables(content *hcl.BodyContent) hcl.Diagnostics { var diags hcl.Diagnostics for _, block := range content.Blocks { switch block.Type { case variableLabel: moreDiags := c.InputVariables.decodeVariableBlock(block, nil) diags = append(diags, moreDiags...) case variablesLabel: attrs, moreDiags := block.Body.JustAttributes() diags = append(diags, moreDiags...) for key, attr := range attrs { moreDiags = c.InputVariables.decodeVariable(key, attr, nil) diags = append(diags, moreDiags...) } } } return diags } // parseLocalVariables looks in the found blocks for 'locals' blocks. It // should be called after parsing input variables so that they can be // referenced. func (c *jobConfig) parseLocalVariables(content *hcl.BodyContent) hcl.Diagnostics { var diags hcl.Diagnostics for _, block := range content.Blocks { switch block.Type { case localsLabel: attrs, moreDiags := block.Body.JustAttributes() diags = append(diags, moreDiags...) for name, attr := range attrs { if _, found := c.LocalVariables[name]; found { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate value in " + localsLabel, Detail: "Duplicate " + name + " definition found.", Subject: attr.NameRange.Ptr(), Context: block.DefRange.Ptr(), }) return diags } c.LocalBlocks = append(c.LocalBlocks, &LocalBlock{ Name: name, Expr: attr.Expr, }) } } } return diags } func (c *jobConfig) decodeTopLevelExtras(content *hcl.BodyContent, ctx *hcl.EvalContext) hcl.Diagnostics { var diags hcl.Diagnostics var foundVault *hcl.Block for _, b := range content.Blocks { if b.Type == vaultLabel { if foundVault != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", b.Type), Detail: fmt.Sprintf( "Only one block of type %q is allowed. Previous definition was at %s.", b.Type, foundVault.DefRange.String(), ), Subject: &b.DefRange, }) continue } foundVault = b v := &api.Vault{} diags = append(diags, hclDecoder.DecodeBody(b.Body, ctx, v)...) c.Vault = v } else if b.Type == taskLabel { t := &api.Task{} diags = append(diags, hclDecoder.DecodeBody(b.Body, ctx, t)...) if len(b.Labels) == 1 { t.Name = b.Labels[0] c.Tasks = append(c.Tasks, t) } } } return diags } func (c *jobConfig) evaluateLocalVariables(locals []*LocalBlock) hcl.Diagnostics { var diags hcl.Diagnostics if len(locals) > 0 && c.LocalVariables == nil { c.LocalVariables = Variables{} } var retry, previousL int for len(locals) > 0 { local := locals[0] moreDiags := c.evaluateLocalVariable(local) if moreDiags.HasErrors() { if len(locals) == 1 { // If this is the only local left there's no need // to try evaluating again return append(diags, moreDiags...) } if previousL == len(locals) { if retry == 100 { // To get to this point, locals must have a circle dependency return append(diags, moreDiags...) } retry++ } previousL = len(locals) // If local uses another local that has not been evaluated yet this could be the reason of errors // Push local to the end of slice to be evaluated later locals = append(locals, local) } else { retry = 0 diags = append(diags, moreDiags...) } // Remove local from slice locals = append(locals[:0], locals[1:]...) } return diags } func (c *jobConfig) evaluateLocalVariable(local *LocalBlock) hcl.Diagnostics { var diags hcl.Diagnostics value, moreDiags := local.Expr.Value(c.EvalContext()) diags = append(diags, moreDiags...) if moreDiags.HasErrors() { return diags } c.LocalVariables[local.Name] = &Variable{ Name: local.Name, Values: []VariableAssignment{{ Value: value, Expr: local.Expr, From: "default", }}, Type: value.Type(), } return diags } func (c *jobConfig) decodeJob(content *hcl.BodyContent, ctx *hcl.EvalContext) hcl.Diagnostics { var diags hcl.Diagnostics c.Job = &api.Job{} var found *hcl.Block for _, b := range content.Blocks { if b.Type != "job" { continue } body := hclutil.BlocksAsAttrs(b.Body) body = dynblock.Expand(body, ctx) if found != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", b.Type), Detail: fmt.Sprintf( "Only one block of type %q is allowed. Previous definition was at %s.", b.Type, found.DefRange.String(), ), Subject: &b.DefRange, }) continue } found = b c.JobID = b.Labels[0] metaAttr, body, mdiags := decodeAsAttribute(body, ctx, "meta") diags = append(diags, mdiags...) extra, remain, mdiags := body.PartialContent(&hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ {Type: "vault"}, {Type: "task", LabelNames: []string{"name"}}, }, }) diags = append(diags, mdiags...) diags = append(diags, c.decodeTopLevelExtras(extra, ctx)...) diags = append(diags, hclDecoder.DecodeBody(remain, ctx, c.Job)...) if metaAttr != nil { c.Job.Meta = metaAttr } } if found == nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing job block", Detail: "A job block is required", }) } return diags } func (c *jobConfig) EvalContext() *hcl.EvalContext { vars, _ := c.InputVariables.Values() locals, _ := c.LocalVariables.Values() return &hcl.EvalContext{ Functions: Functions(c.ParseConfig.BaseDir, c.ParseConfig.AllowFS), Variables: map[string]cty.Value{ inputVariablesAccessor: cty.ObjectVal(vars), localsAccessor: cty.ObjectVal(locals), }, UndefinedVariable: func(t hcl.Traversal) (cty.Value, hcl.Diagnostics) { body := c.ParseConfig.Body start := t.SourceRange().Start.Byte end := t.SourceRange().End.Byte v := string(body[start:end]) // find the start of inclusing "${..}" if it's inclused in one; otherwise, wrap it in one isBracketed := false for i := start - 1; i >= 1; i-- { if body[i] == '{' && body[i-1] == '$' { isBracketed = true v = string(body[i-1:start]) + v break } else if body[i] != ' ' { break } } if isBracketed { for i := end + 1; i < len(body); i++ { if body[i] == '}' { v += string(body[end:i]) } else if body[i] != ' ' { // unexpected! v += "}" break } } } else { v = "${" + v + "}" } return cty.StringVal(v), nil }, } } func toVars(vars []string) map[string]string { attrs := make(map[string]string, len(vars)) for _, arg := range vars { parts := strings.SplitN(arg, "=", 2) if len(parts) == 2 { attrs[parts[0]] = parts[1] } } return attrs }