2020-10-21 14:19:46 +00:00
|
|
|
package jobspec2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
2020-11-09 19:58:57 +00:00
|
|
|
"fmt"
|
2020-10-21 14:19:46 +00:00
|
|
|
"io"
|
2020-11-09 19:58:57 +00:00
|
|
|
"io/ioutil"
|
2020-10-21 14:19:46 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/hashicorp/hcl/v2/ext/dynblock"
|
|
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
|
|
hcljson "github.com/hashicorp/hcl/v2/json"
|
|
|
|
"github.com/hashicorp/nomad/api"
|
|
|
|
"github.com/hashicorp/nomad/jobspec2/hclutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
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
|
2020-11-09 19:58:57 +00:00
|
|
|
_, err := io.Copy(&buf, r)
|
|
|
|
if err != nil {
|
2020-10-21 14:19:46 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-11-09 19:58:57 +00:00
|
|
|
return ParseWithConfig(&ParseConfig{
|
|
|
|
Path: path,
|
|
|
|
Body: buf.Bytes(),
|
|
|
|
AllowFS: false,
|
|
|
|
Strict: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func ParseWithConfig(args *ParseConfig) (*api.Job, error) {
|
|
|
|
args.normalize()
|
|
|
|
|
2020-11-09 21:27:06 +00:00
|
|
|
c := newJobConfig(args)
|
2020-11-09 19:58:57 +00:00
|
|
|
err := decode(c)
|
2020-10-21 14:19:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-11-09 19:58:57 +00:00
|
|
|
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 accecss
|
|
|
|
AllowFS bool
|
|
|
|
|
|
|
|
// ArgVars is the CLI -var arguments
|
|
|
|
ArgVars []string
|
|
|
|
|
|
|
|
// VarFiles is the paths of variable data files
|
|
|
|
VarFiles []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)
|
|
|
|
}
|
2020-10-21 14:19:46 +00:00
|
|
|
}
|
|
|
|
|
2020-11-09 19:58:57 +00:00
|
|
|
func decode(c *jobConfig) error {
|
2020-10-21 14:19:46 +00:00
|
|
|
var file *hcl.File
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
|
2020-11-09 19:58:57 +00:00
|
|
|
pc := c.ParseConfig
|
|
|
|
|
|
|
|
if !isJSON(pc.Body) {
|
|
|
|
file, diags = hclsyntax.ParseConfig(pc.Body, pc.Path, hcl.Pos{Line: 1, Column: 1})
|
2020-10-21 14:19:46 +00:00
|
|
|
} else {
|
2020-11-09 19:58:57 +00:00
|
|
|
file, diags = hcljson.Parse(pc.Body, pc.Path)
|
2020-10-21 14:19:46 +00:00
|
|
|
|
|
|
|
}
|
2020-11-09 20:02:21 +00:00
|
|
|
|
|
|
|
parsedVarFiles, mdiags := parseVarFiles(pc.VarFiles)
|
|
|
|
pc.parsedVarFiles = parsedVarFiles
|
|
|
|
diags = append(diags, mdiags...)
|
|
|
|
|
2020-10-21 14:19:46 +00:00
|
|
|
if diags.HasErrors() {
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
|
|
|
|
body := hclutil.BlocksAsAttrs(file.Body)
|
2020-11-09 19:58:57 +00:00
|
|
|
body = dynblock.Expand(body, c.EvalContext())
|
|
|
|
diags = c.decodeBody(body)
|
2020-10-21 14:19:46 +00:00
|
|
|
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())
|
|
|
|
}
|
2020-11-09 19:58:57 +00:00
|
|
|
diags = append(diags, decodeMapInterfaceType(&c, c.EvalContext())...)
|
2020-10-21 14:19:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-09 20:02:21 +00:00
|
|
|
func parseVarFiles(paths []string) ([]*hcl.File, hcl.Diagnostics) {
|
|
|
|
if len(paths) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
files := make([]*hcl.File, 0, len(paths))
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
|
|
|
|
for _, p := range paths {
|
|
|
|
body, err := ioutil.ReadFile(p)
|
|
|
|
if err != nil {
|
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Failed to read file",
|
|
|
|
Detail: fmt.Sprintf("failed to read %q: %v", p, err),
|
|
|
|
})
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var file *hcl.File
|
|
|
|
var mdiags hcl.Diagnostics
|
|
|
|
if !isJSON(body) {
|
|
|
|
file, mdiags = hclsyntax.ParseConfig(body, p, hcl.Pos{Line: 1, Column: 1})
|
|
|
|
} else {
|
|
|
|
file, mdiags = hcljson.Parse(body, p)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
files = append(files, file)
|
|
|
|
diags = append(diags, mdiags...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return files, diags
|
|
|
|
}
|
|
|
|
|
2020-10-21 14:19:46 +00:00
|
|
|
func isJSON(src []byte) bool {
|
|
|
|
for _, c := range src {
|
|
|
|
if c == ' ' {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
return c == '{'
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|