open-nomad/jobspec2/parse.go

177 lines
3.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package jobspec2
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hcljson "github.com/hashicorp/hcl/v2/json"
"github.com/hashicorp/nomad/api"
)
func Parse(path string, r io.Reader) (*api.Job, error) {
if path == "" {
if f, ok := r.(*os.File); ok {
path = f.Name()
}
}
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
return nil, err
}
return ParseWithConfig(&ParseConfig{
Path: path,
Body: buf.Bytes(),
AllowFS: false,
Strict: true,
})
}
func ParseWithConfig(args *ParseConfig) (*api.Job, error) {
args.normalize()
c := newJobConfig(args)
err := decode(c)
if err != nil {
return nil, err
}
normalizeJob(c)
return c.Job, nil
}
type ParseConfig struct {
Path string
BaseDir string
// Body is the HCL body
Body []byte
// AllowFS enables HCL functions that require file system access
AllowFS bool
// ArgVars is the CLI -var arguments
ArgVars []string
// VarFiles is the paths of variable data files that should be read during
// parsing.
VarFiles []string
// VarContent is the content of variable data known without reading an
// actual var file during parsing.
VarContent string
// Envs represent process environment variable
Envs []string
Strict bool
// parsedVarFiles represent parsed HCL AST of the passed EnvVars
parsedVarFiles []*hcl.File
}
func (c *ParseConfig) normalize() {
if c.BaseDir == "" {
c.BaseDir = filepath.Dir(c.Path)
}
}
func decode(c *jobConfig) error {
config := c.ParseConfig
file, diags := parseHCLOrJSON(config.Body, config.Path)
for _, varFile := range config.VarFiles {
parsedVarFile, ds := parseFile(varFile)
if parsedVarFile == nil || ds.HasErrors() {
return fmt.Errorf("unable to parse var file: %v", ds.Error())
}
config.parsedVarFiles = append(config.parsedVarFiles, parsedVarFile)
diags = append(diags, ds...)
}
if config.VarContent != "" {
hclFile, hclDiagnostics := parseHCLOrJSON([]byte(config.VarContent), "input.hcl")
if hclDiagnostics.HasErrors() {
return fmt.Errorf("unable to parse var content: %v", hclDiagnostics.Error())
}
config.parsedVarFiles = append(config.parsedVarFiles, hclFile)
}
// Return early if the input job or variable files are not valid.
// Decoding and evaluating invalid files may result in unexpected results.
if diags.HasErrors() {
return diags
}
diags = append(diags, c.decodeBody(file.Body)...)
if diags.HasErrors() {
var str strings.Builder
for i, diag := range diags {
if i != 0 {
str.WriteByte('\n')
}
str.WriteString(diag.Error())
}
return errors.New(str.String())
}
diags = append(diags, decodeMapInterfaceType(&c.Job, c.EvalContext())...)
diags = append(diags, decodeMapInterfaceType(&c.Tasks, c.EvalContext())...)
diags = append(diags, decodeMapInterfaceType(&c.Vault, c.EvalContext())...)
if diags.HasErrors() {
return diags
}
return nil
}
func parseFile(path string) (*hcl.File, hcl.Diagnostics) {
body, err := os.ReadFile(path)
if err != nil {
return nil, hcl.Diagnostics{
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to read file",
Detail: fmt.Sprintf("failed to read %q: %v", path, err),
},
}
}
return parseHCLOrJSON(body, path)
}
func parseHCLOrJSON(src []byte, filename string) (*hcl.File, hcl.Diagnostics) {
if isJSON(src) {
return hcljson.Parse(src, filename)
}
return hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1})
}
func isJSON(src []byte) bool {
for _, c := range src {
if c == ' ' {
continue
}
return c == '{'
}
return false
}