diff --git a/bake/bake.go b/bake/bake.go index edc8c501..bb0d4578 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -2,6 +2,7 @@ package bake import ( "context" + "fmt" "io/ioutil" "os" "path" @@ -9,6 +10,7 @@ import ( "strconv" "strings" + "github.com/docker/buildx/bake/hclparser" "github.com/docker/buildx/build" "github.com/docker/buildx/util/buildflags" "github.com/docker/buildx/util/platformutil" @@ -60,7 +62,7 @@ func ReadLocalFiles(names []string) ([]File, error) { } func ReadTargets(ctx context.Context, files []File, targets, overrides []string) (map[string]*Target, error) { - c, err := parseFiles(files) + c, err := ParseFiles(files) if err != nil { return nil, err } @@ -84,29 +86,43 @@ func ReadTargets(ctx context.Context, files []File, targets, overrides []string) return m, nil } -func parseFiles(files []File) (*Config, error) { +func ParseFiles(files []File) (_ *Config, err error) { + defer func() { + err = formatHCLError(err, files) + }() + var c Config var fs []*hcl.File - var scs []*StaticConfig for _, f := range files { - cfg, f, sc, err := parseFile(f.Data, f.Name) - if err != nil { - return nil, err - } - if cfg != nil { + cfg, isCompose, composeErr := ParseComposeFile(f.Data, f.Name) + if isCompose { + if composeErr != nil { + return nil, composeErr + } c = mergeConfig(c, *cfg) - } else { - fs = append(fs, f) - scs = append(scs, sc) + c = dedupeConfig(c) + } + if !isCompose { + hf, isHCL, err := ParseHCLFile(f.Data, f.Name) + if isHCL { + if err != nil { + return nil, err + } + fs = append(fs, hf) + } else if composeErr != nil { + return nil, fmt.Errorf("failed to parse %s: parsing yaml: %v, parsing hcl: %w", f.Name, composeErr, err) + } else { + return nil, err + } } } if len(fs) > 0 { - cfg, err := ParseHCL(hcl.MergeFiles(fs), mergeStaticConfig(scs)) - if err != nil { + if err := hclparser.Parse(hcl.MergeFiles(fs), hclparser.Opt{ + LookupVar: os.LookupEnv, + }, &c); err.HasErrors() { return nil, err } - c = mergeConfig(c, dedupeConfig(*cfg)) } return &c, nil } @@ -117,7 +133,7 @@ func dedupeConfig(c Config) Config { m := map[string]*Target{} for _, t := range c.Targets { if t2, ok := m[t.Name]; ok { - merge(t2, t) + t2.Merge(t) } else { m[t.Name] = t c2.Targets = append(c2.Targets, t) @@ -127,37 +143,25 @@ func dedupeConfig(c Config) Config { } func ParseFile(dt []byte, fn string) (*Config, error) { - return parseFiles([]File{{Data: dt, Name: fn}}) + return ParseFiles([]File{{Data: dt, Name: fn}}) } -func parseFile(dt []byte, fn string) (*Config, *hcl.File, *StaticConfig, error) { +func ParseComposeFile(dt []byte, fn string) (*Config, bool, error) { fnl := strings.ToLower(fn) if strings.HasSuffix(fnl, ".yml") || strings.HasSuffix(fnl, ".yaml") { - c, err := ParseCompose(dt) - return c, nil, nil, err + cfg, err := ParseCompose(dt) + return cfg, true, err } - if strings.HasSuffix(fnl, ".json") || strings.HasSuffix(fnl, ".hcl") { - f, sc, err := ParseHCLFile(dt, fn) - return nil, f, sc, err + return nil, false, nil } - cfg, err := ParseCompose(dt) - if err != nil { - f, sc, err2 := ParseHCLFile(dt, fn) - if err2 != nil { - return nil, nil, nil, errors.Errorf("failed to parse %s: parsing yaml: %s, parsing hcl: %s", fn, err.Error(), err2.Error()) - } - return nil, f, sc, nil - } - return cfg, nil, nil, nil + return cfg, err == nil, err } type Config struct { - Variables []*Variable `json:"-" hcl:"variable,block"` - Groups []*Group `json:"group" hcl:"group,block"` - Targets []*Target `json:"target" hcl:"target,block"` - Remain hcl.Body `json:"-" hcl:",remain"` + Groups []*Group `json:"group" hcl:"group,block"` + Targets []*Target `json:"target" hcl:"target,block"` } func mergeConfig(c1, c2 Config) Config { @@ -203,7 +207,8 @@ func mergeConfig(c1, c2 Config) Config { } } if t1 != nil { - t2 = merge(t1, t2) + t1.Merge(t2) + t2 = t1 } c1.Targets = append(c1.Targets, t2) } @@ -390,30 +395,21 @@ func (c Config) target(name string, visited map[string]struct{}, overrides map[s return nil, err } if t != nil { - tt = merge(tt, t) + tt.Merge(t) } } t.Inherits = nil - tt = merge(merge(defaultTarget(), tt), t) + m := defaultTarget() + m.Merge(tt) + m.Merge(t) + tt = m if override, ok := overrides[name]; ok { - tt = merge(tt, override) + tt.Merge(override) } tt.normalize() return tt, nil } -type Variable struct { - Name string `json:"-" hcl:"name,label"` - Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"` -} - -type Function struct { - Name string `json:"-" hcl:"name,label"` - Params *hcl.Attribute `json:"params,omitempty" hcl:"params"` - Variadic *hcl.Attribute `json:"variadic_param,omitempty" hcl:"variadic_params"` - Result *hcl.Attribute `json:"result,omitempty" hcl:"result"` -} - type Group struct { Name string `json:"-" hcl:"name,label"` Targets []string `json:"targets" hcl:"targets"` @@ -455,6 +451,61 @@ func (t *Target) normalize() { t.Outputs = removeDupes(t.Outputs) } +func (t *Target) Merge(t2 *Target) { + if t2.Context != nil { + t.Context = t2.Context + } + if t2.Dockerfile != nil { + t.Dockerfile = t2.Dockerfile + } + if t2.DockerfileInline != nil { + t.DockerfileInline = t2.DockerfileInline + } + for k, v := range t2.Args { + if t.Args == nil { + t.Args = map[string]string{} + } + t.Args[k] = v + } + for k, v := range t2.Labels { + if t.Labels == nil { + t.Labels = map[string]string{} + } + t.Labels[k] = v + } + if t2.Tags != nil { // no merge + t.Tags = t2.Tags + } + if t2.Target != nil { + t.Target = t2.Target + } + if t2.Secrets != nil { // merge + t.Secrets = append(t.Secrets, t2.Secrets...) + } + if t2.SSH != nil { // merge + t.SSH = append(t.SSH, t2.SSH...) + } + if t2.Platforms != nil { // no merge + t.Platforms = t2.Platforms + } + if t2.CacheFrom != nil { // merge + t.CacheFrom = append(t.CacheFrom, t2.CacheFrom...) + } + if t2.CacheTo != nil { // no merge + t.CacheTo = t2.CacheTo + } + if t2.Outputs != nil { // no merge + t.Outputs = t2.Outputs + } + if t2.Pull != nil { + t.Pull = t2.Pull + } + if t2.NoCache != nil { + t.NoCache = t2.NoCache + } + t.Inherits = append(t.Inherits, t2.Inherits...) +} + func TargetsToBuildOpt(m map[string]*Target, inp *Input) (map[string]build.Options, error) { m2 := make(map[string]build.Options, len(m)) for k, v := range m { @@ -581,62 +632,6 @@ func defaultTarget() *Target { return &Target{} } -func merge(t1, t2 *Target) *Target { - if t2.Context != nil { - t1.Context = t2.Context - } - if t2.Dockerfile != nil { - t1.Dockerfile = t2.Dockerfile - } - if t2.DockerfileInline != nil { - t1.DockerfileInline = t2.DockerfileInline - } - for k, v := range t2.Args { - if t1.Args == nil { - t1.Args = map[string]string{} - } - t1.Args[k] = v - } - for k, v := range t2.Labels { - if t1.Labels == nil { - t1.Labels = map[string]string{} - } - t1.Labels[k] = v - } - if t2.Tags != nil { // no merge - t1.Tags = t2.Tags - } - if t2.Target != nil { - t1.Target = t2.Target - } - if t2.Secrets != nil { // merge - t1.Secrets = append(t1.Secrets, t2.Secrets...) - } - if t2.SSH != nil { // merge - t1.SSH = append(t1.SSH, t2.SSH...) - } - if t2.Platforms != nil { // no merge - t1.Platforms = t2.Platforms - } - if t2.CacheFrom != nil { // no merge - t1.CacheFrom = append(t1.CacheFrom, t2.CacheFrom...) - } - if t2.CacheTo != nil { // no merge - t1.CacheTo = t2.CacheTo - } - if t2.Outputs != nil { // no merge - t1.Outputs = t2.Outputs - } - if t2.Pull != nil { - t1.Pull = t2.Pull - } - if t2.NoCache != nil { - t1.NoCache = t2.NoCache - } - t1.Inherits = append(t1.Inherits, t2.Inherits...) - return t1 -} - func removeDupes(s []string) []string { i := 0 seen := make(map[string]struct{}, len(s)) diff --git a/bake/hcl.go b/bake/hcl.go index 6e0ff41e..c0b321af 100644 --- a/bake/hcl.go +++ b/bake/hcl.go @@ -1,618 +1,42 @@ package bake import ( - "math" - "math/big" - "os" - "reflect" - "strconv" "strings" - "unsafe" - "github.com/docker/buildx/util/userfunc" - "github.com/hashicorp/go-cty-funcs/cidr" - "github.com/hashicorp/go-cty-funcs/crypto" - "github.com/hashicorp/go-cty-funcs/encoding" - "github.com/hashicorp/go-cty-funcs/uuid" hcl "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/ext/tryfunc" - "github.com/hashicorp/hcl/v2/ext/typeexpr" - "github.com/hashicorp/hcl/v2/gohcl" - "github.com/hashicorp/hcl/v2/hclsyntax" - hcljson "github.com/hashicorp/hcl/v2/json" + "github.com/hashicorp/hcl/v2/hclparse" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/solver/pb" - "github.com/pkg/errors" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/function" - "github.com/zclconf/go-cty/cty/function/stdlib" ) -// Collection of generally useful functions in cty-using applications, which -// HCL supports. These functions are available for use in HCL files. -var ( - stdlibFunctions = map[string]function.Function{ - "absolute": stdlib.AbsoluteFunc, - "add": stdlib.AddFunc, - "and": stdlib.AndFunc, - "base64decode": encoding.Base64DecodeFunc, - "base64encode": encoding.Base64EncodeFunc, - "bcrypt": crypto.BcryptFunc, - "byteslen": stdlib.BytesLenFunc, - "bytesslice": stdlib.BytesSliceFunc, - "can": tryfunc.CanFunc, - "ceil": stdlib.CeilFunc, - "chomp": stdlib.ChompFunc, - "chunklist": stdlib.ChunklistFunc, - "cidrhost": cidr.HostFunc, - "cidrnetmask": cidr.NetmaskFunc, - "cidrsubnet": cidr.SubnetFunc, - "cidrsubnets": cidr.SubnetsFunc, - "csvdecode": stdlib.CSVDecodeFunc, - "coalesce": stdlib.CoalesceFunc, - "coalescelist": stdlib.CoalesceListFunc, - "compact": stdlib.CompactFunc, - "concat": stdlib.ConcatFunc, - "contains": stdlib.ContainsFunc, - "convert": typeexpr.ConvertFunc, - "distinct": stdlib.DistinctFunc, - "divide": stdlib.DivideFunc, - "element": stdlib.ElementFunc, - "equal": stdlib.EqualFunc, - "flatten": stdlib.FlattenFunc, - "floor": stdlib.FloorFunc, - "formatdate": stdlib.FormatDateFunc, - "format": stdlib.FormatFunc, - "formatlist": stdlib.FormatListFunc, - "greaterthan": stdlib.GreaterThanFunc, - "greaterthanorequalto": stdlib.GreaterThanOrEqualToFunc, - "hasindex": stdlib.HasIndexFunc, - "indent": stdlib.IndentFunc, - "index": stdlib.IndexFunc, - "int": stdlib.IntFunc, - "jsondecode": stdlib.JSONDecodeFunc, - "jsonencode": stdlib.JSONEncodeFunc, - "keys": stdlib.KeysFunc, - "join": stdlib.JoinFunc, - "length": stdlib.LengthFunc, - "lessthan": stdlib.LessThanFunc, - "lessthanorequalto": stdlib.LessThanOrEqualToFunc, - "log": stdlib.LogFunc, - "lookup": stdlib.LookupFunc, - "lower": stdlib.LowerFunc, - "max": stdlib.MaxFunc, - "md5": crypto.Md5Func, - "merge": stdlib.MergeFunc, - "min": stdlib.MinFunc, - "modulo": stdlib.ModuloFunc, - "multiply": stdlib.MultiplyFunc, - "negate": stdlib.NegateFunc, - "notequal": stdlib.NotEqualFunc, - "not": stdlib.NotFunc, - "or": stdlib.OrFunc, - "parseint": stdlib.ParseIntFunc, - "pow": stdlib.PowFunc, - "range": stdlib.RangeFunc, - "regexall": stdlib.RegexAllFunc, - "regex": stdlib.RegexFunc, - "regex_replace": stdlib.RegexReplaceFunc, - "reverse": stdlib.ReverseFunc, - "reverselist": stdlib.ReverseListFunc, - "rsadecrypt": crypto.RsaDecryptFunc, - "sethaselement": stdlib.SetHasElementFunc, - "setintersection": stdlib.SetIntersectionFunc, - "setproduct": stdlib.SetProductFunc, - "setsubtract": stdlib.SetSubtractFunc, - "setsymmetricdifference": stdlib.SetSymmetricDifferenceFunc, - "setunion": stdlib.SetUnionFunc, - "sha1": crypto.Sha1Func, - "sha256": crypto.Sha256Func, - "sha512": crypto.Sha512Func, - "signum": stdlib.SignumFunc, - "slice": stdlib.SliceFunc, - "sort": stdlib.SortFunc, - "split": stdlib.SplitFunc, - "strlen": stdlib.StrlenFunc, - "substr": stdlib.SubstrFunc, - "subtract": stdlib.SubtractFunc, - "timeadd": stdlib.TimeAddFunc, - "title": stdlib.TitleFunc, - "trim": stdlib.TrimFunc, - "trimprefix": stdlib.TrimPrefixFunc, - "trimspace": stdlib.TrimSpaceFunc, - "trimsuffix": stdlib.TrimSuffixFunc, - "try": tryfunc.TryFunc, - "upper": stdlib.UpperFunc, - "urlencode": encoding.URLEncodeFunc, - "uuidv4": uuid.V4Func, - "uuidv5": uuid.V5Func, - "values": stdlib.ValuesFunc, - "zipmap": stdlib.ZipmapFunc, - } -) - -type StaticConfig struct { - Variables []*Variable `hcl:"variable,block"` - Functions []*Function `hcl:"function,block"` - Remain hcl.Body `hcl:",remain"` - - attrs hcl.Attributes - - defaults map[string]*hcl.Attribute - funcDefs map[string]*Function - funcs map[string]function.Function - env map[string]string - ectx hcl.EvalContext - progress map[string]struct{} - progressF map[string]struct{} -} - -func mergeStaticConfig(scs []*StaticConfig) *StaticConfig { - if len(scs) == 0 { - return nil - } - sc := scs[0] - for _, s := range scs[1:] { - sc.Variables = append(sc.Variables, s.Variables...) - sc.Functions = append(sc.Functions, s.Functions...) - for k, v := range s.attrs { - sc.attrs[k] = v - } - } - return sc -} - -func (sc *StaticConfig) EvalContext(withEnv bool) (*hcl.EvalContext, error) { - // json parser also parses blocks as attributes - delete(sc.attrs, "target") - delete(sc.attrs, "function") - - sc.defaults = map[string]*hcl.Attribute{} - for _, v := range sc.Variables { - if v.Name == "target" { - continue - } - sc.defaults[v.Name] = v.Default - } - - sc.env = map[string]string{} - if withEnv { - // Override default with values from environment. - for _, v := range os.Environ() { - parts := strings.SplitN(v, "=", 2) - name, value := parts[0], parts[1] - sc.env[name] = value - } - } - - sc.funcDefs = map[string]*Function{} - for _, v := range sc.Functions { - sc.funcDefs[v.Name] = v - } - - sc.ectx = hcl.EvalContext{ - Variables: map[string]cty.Value{}, - Functions: stdlibFunctions, - } - sc.funcs = map[string]function.Function{} - sc.progress = map[string]struct{}{} - sc.progressF = map[string]struct{}{} - for k := range sc.attrs { - if err := sc.resolveValue(k); err != nil { - return nil, err - } - } - - for k := range sc.defaults { - if err := sc.resolveValue(k); err != nil { - return nil, err - } - } - - for k := range sc.funcDefs { - if err := sc.resolveFunction(k); err != nil { - return nil, err - } - } - return &sc.ectx, nil -} - -type jsonExp interface { - ExprList() []hcl.Expression - ExprMap() []hcl.KeyValuePair -} - -func elementExpressions(je jsonExp, exp hcl.Expression) []hcl.Expression { - list := je.ExprList() - if len(list) != 0 { - exp := make([]hcl.Expression, 0, len(list)) - for _, e := range list { - if je, ok := e.(jsonExp); ok { - exp = append(exp, elementExpressions(je, e)...) - } - } - return exp - } - kvlist := je.ExprMap() - if len(kvlist) != 0 { - exp := make([]hcl.Expression, 0, len(kvlist)*2) - for _, p := range kvlist { - exp = append(exp, p.Key) - if je, ok := p.Value.(jsonExp); ok { - exp = append(exp, elementExpressions(je, p.Value)...) - } - } - return exp - } - return []hcl.Expression{exp} -} - -func jsonFuncCallsRecursive(exp hcl.Expression) ([]string, error) { - je, ok := exp.(jsonExp) - if !ok { - return nil, errors.Errorf("invalid expression type %T", exp) - } - m := map[string]struct{}{} - for _, e := range elementExpressions(je, exp) { - if err := appendJSONFuncCalls(e, m); err != nil { - return nil, err - } - } - arr := make([]string, 0, len(m)) - for n := range m { - arr = append(arr, n) - } - return arr, nil -} - -func appendJSONFuncCalls(exp hcl.Expression, m map[string]struct{}) error { - v := reflect.ValueOf(exp) - if v.Kind() != reflect.Ptr || v.IsNil() { - return errors.Errorf("invalid json expression kind %T %v", exp, v.Kind()) - } - if v.Elem().Kind() != reflect.Struct { - return errors.Errorf("invalid json expression pointer to %T %v", exp, v.Elem().Kind()) - } - src := v.Elem().FieldByName("src") - if src.IsZero() { - return errors.Errorf("%v has no property src", v.Elem().Type()) - } - if src.Kind() != reflect.Interface { - return errors.Errorf("%v src is not interface: %v", src.Type(), src.Kind()) - } - src = src.Elem() - if src.IsNil() { - return nil - } - if src.Kind() == reflect.Ptr { - src = src.Elem() - } - if src.Kind() != reflect.Struct { - return errors.Errorf("%v is not struct: %v", src.Type(), src.Kind()) - } - - // hcl/v2/json/ast#stringVal - val := src.FieldByName("Value") - if val.IsZero() { - return nil - } - rng := src.FieldByName("SrcRange") - if val.IsZero() { - return nil - } - var stringVal struct { - Value string - SrcRange hcl.Range - } - - if !val.Type().AssignableTo(reflect.ValueOf(stringVal.Value).Type()) { - return nil - } - if !rng.Type().AssignableTo(reflect.ValueOf(stringVal.SrcRange).Type()) { - return nil - } - // reflect.Set does not work for unexported fields - stringVal.Value = *(*string)(unsafe.Pointer(val.UnsafeAddr())) - stringVal.SrcRange = *(*hcl.Range)(unsafe.Pointer(rng.UnsafeAddr())) - - expr, diags := hclsyntax.ParseExpression([]byte(stringVal.Value), stringVal.SrcRange.Filename, stringVal.SrcRange.Start) - if diags.HasErrors() { - return nil - } - - fns, err := funcCalls(expr) - if err != nil { - return err - } - - for _, fn := range fns { - m[fn] = struct{}{} - } - - return nil -} - -func funcCalls(exp hcl.Expression) ([]string, hcl.Diagnostics) { - node, ok := exp.(hclsyntax.Node) - if !ok { - fns, err := jsonFuncCallsRecursive(exp) - if err != nil { - return nil, hcl.Diagnostics{ - &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid expression", - Detail: err.Error(), - Subject: exp.Range().Ptr(), - Context: exp.Range().Ptr(), - }, - } - } - return fns, nil - } - - var funcnames []string - hcldiags := hclsyntax.VisitAll(node, func(n hclsyntax.Node) hcl.Diagnostics { - if fe, ok := n.(*hclsyntax.FunctionCallExpr); ok { - funcnames = append(funcnames, fe.Name) - } - return nil - }) - if hcldiags.HasErrors() { - return nil, hcldiags - } - return funcnames, nil -} - -func (sc *StaticConfig) loadDeps(exp hcl.Expression, exclude map[string]struct{}) hcl.Diagnostics { - fns, hcldiags := funcCalls(exp) - if hcldiags.HasErrors() { - return hcldiags - } - - for _, fn := range fns { - if err := sc.resolveFunction(fn); err != nil { - return hcl.Diagnostics{ - &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid expression", - Detail: err.Error(), - Subject: exp.Range().Ptr(), - Context: exp.Range().Ptr(), - }, - } - } - } - - for _, v := range exp.Variables() { - if _, ok := exclude[v.RootName()]; ok { - continue - } - if err := sc.resolveValue(v.RootName()); err != nil { - return hcl.Diagnostics{ - &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid expression", - Detail: err.Error(), - Subject: v.SourceRange().Ptr(), - Context: v.SourceRange().Ptr(), - }, - } - } - } - - return nil -} - -func (sc *StaticConfig) resolveFunction(name string) error { - if _, ok := sc.funcs[name]; ok { - return nil - } - f, ok := sc.funcDefs[name] - if !ok { - if _, ok := sc.ectx.Functions[name]; ok { - return nil - } - return errors.Errorf("undefined function %s", name) - } - if _, ok := sc.progressF[name]; ok { - return errors.Errorf("function cycle not allowed for %s", name) - } - sc.progressF[name] = struct{}{} - - paramExprs, paramsDiags := hcl.ExprList(f.Params.Expr) - if paramsDiags.HasErrors() { - return paramsDiags - } - var diags hcl.Diagnostics - params := map[string]struct{}{} - for _, paramExpr := range paramExprs { - param := hcl.ExprAsKeyword(paramExpr) - if param == "" { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid param element", - Detail: "Each parameter name must be an identifier.", - Subject: paramExpr.Range().Ptr(), - }) - } - params[param] = struct{}{} - } - var variadic hcl.Expression - if f.Variadic != nil { - variadic = f.Variadic.Expr - param := hcl.ExprAsKeyword(variadic) - if param == "" { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid param element", - Detail: "Each parameter name must be an identifier.", - Subject: f.Variadic.Range.Ptr(), - }) - } - params[param] = struct{}{} - } - if diags.HasErrors() { - return diags - } - - if diags := sc.loadDeps(f.Result.Expr, params); diags.HasErrors() { - return diags - } - - v, diags := userfunc.NewFunction(f.Params.Expr, variadic, f.Result.Expr, func() *hcl.EvalContext { - return &sc.ectx - }) - if diags.HasErrors() { - return diags - } - sc.funcs[name] = v - sc.ectx.Functions[name] = v - - return nil -} - -func (sc *StaticConfig) resolveValue(name string) (err error) { - if _, ok := sc.ectx.Variables[name]; ok { - return nil - } - if _, ok := sc.progress[name]; ok { - return errors.Errorf("variable cycle not allowed for %s", name) - } - sc.progress[name] = struct{}{} - - var v *cty.Value - defer func() { - if v != nil { - sc.ectx.Variables[name] = *v +func ParseHCLFile(dt []byte, fn string) (*hcl.File, bool, error) { + var err error + if strings.HasSuffix(fn, ".json") { + f, diags := hclparse.NewParser().ParseJSON(dt, fn) + if diags.HasErrors() { + err = diags } - }() - - def, ok := sc.attrs[name] - if !ok { - def, ok = sc.defaults[name] - if !ok { - return errors.Errorf("undefined variable %q", name) - } - } - - if def == nil { - vv := cty.StringVal(sc.env[name]) - v = &vv - return + return f, true, err } - - if diags := sc.loadDeps(def.Expr, nil); diags.HasErrors() { - return diags - } - vv, diags := def.Expr.Value(&sc.ectx) - if diags.HasErrors() { - return diags - } - - _, isVar := sc.defaults[name] - - if envv, ok := sc.env[name]; ok && isVar { - if vv.Type().Equals(cty.Bool) { - b, err := strconv.ParseBool(envv) - if err != nil { - return errors.Wrapf(err, "failed to parse %s as bool", name) - } - vv := cty.BoolVal(b) - v = &vv - return nil - } else if vv.Type().Equals(cty.String) { - vv := cty.StringVal(envv) - v = &vv - return nil - } else if vv.Type().Equals(cty.Number) { - n, err := strconv.ParseFloat(envv, 64) - if err == nil && (math.IsNaN(n) || math.IsInf(n, 0)) { - err = errors.Errorf("invalid number value") - } - if err != nil { - return errors.Wrapf(err, "failed to parse %s as number", name) - } - vv := cty.NumberVal(big.NewFloat(n)) - v = &vv - return nil - } else { - // TODO: support lists with csv values - return errors.Errorf("unsupported type %s for variable %s", v.Type(), name) + if strings.HasSuffix(fn, ".hcl") { + f, diags := hclparse.NewParser().ParseHCL(dt, fn) + if diags.HasErrors() { + err = diags } + return f, true, err } - v = &vv - return nil -} - -func ParseHCLFile(dt []byte, fn string) (*hcl.File, *StaticConfig, error) { - if strings.HasSuffix(fn, ".json") || strings.HasSuffix(fn, ".hcl") { - return parseHCLFile(dt, fn) - } - f, sc, err := parseHCLFile(dt, fn+".hcl") - if err != nil { - f, sc, err2 := parseHCLFile(dt, fn+".json") - if err2 == nil { - return f, sc, nil - } - } - return f, sc, err -} - -func parseHCLFile(dt []byte, fn string) (f *hcl.File, _ *StaticConfig, err error) { - defer func() { - err = formatHCLError(dt, err) - }() - - // Decode user defined functions, first parsing as hcl and falling back to - // json, returning errors based on the file suffix. - f, hcldiags := hclsyntax.ParseConfig(dt, fn, hcl.Pos{Line: 1, Column: 1}) - if hcldiags.HasErrors() { - var jsondiags hcl.Diagnostics - f, jsondiags = hcljson.Parse(dt, fn) - if jsondiags.HasErrors() { - fnl := strings.ToLower(fn) - if strings.HasSuffix(fnl, ".json") { - return nil, nil, jsondiags - } - return nil, nil, hcldiags - } - } - - var sc StaticConfig - // Decode only variable blocks without interpolation. - if err := gohcl.DecodeBody(f.Body, nil, &sc); err != nil { - return nil, nil, err - } - - attrs, diags := f.Body.JustAttributes() + f, diags := hclparse.NewParser().ParseHCL(dt, fn+".hcl") if diags.HasErrors() { - for _, d := range diags { - if d.Detail != "Blocks are not allowed here." { - return nil, nil, diags - } + f, diags2 := hclparse.NewParser().ParseJSON(dt, fn+".json") + if !diags2.HasErrors() { + return f, true, nil } + return nil, false, diags } - sc.attrs = attrs - - return f, &sc, nil -} - -func ParseHCL(b hcl.Body, sc *StaticConfig) (_ *Config, err error) { - ctx, err := sc.EvalContext(true) - if err != nil { - return nil, err - } - - var c Config - - // Decode with variables and functions. - if err := gohcl.DecodeBody(b, ctx, &c); err != nil { - return nil, err - } - return &c, nil + return f, true, nil } -func formatHCLError(dt []byte, err error) error { +func formatHCLError(err error, files []File) error { if err == nil { return nil } @@ -625,6 +49,13 @@ func formatHCLError(dt []byte, err error) error { continue } if d.Subject != nil { + var dt []byte + for _, f := range files { + if d.Subject.Filename == f.Name { + dt = f.Data + break + } + } src := errdefs.Source{ Info: &pb.SourceInfo{ Filename: d.Subject.Filename, diff --git a/bake/hcl_test.go b/bake/hcl_test.go index 6f30aae1..140293c5 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -44,7 +44,6 @@ func TestHCLBasic(t *testing.T) { c, err := ParseFile(dt, "docker-bake.hcl") require.NoError(t, err) - require.Equal(t, 1, len(c.Groups)) require.Equal(t, "default", c.Groups[0].Name) require.Equal(t, []string{"db", "webapp"}, c.Groups[0].Targets) @@ -274,7 +273,7 @@ func TestHCLMultiFileSharedVariables(t *testing.T) { } `) - c, err := parseFiles([]File{ + c, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }) @@ -286,7 +285,7 @@ func TestHCLMultiFileSharedVariables(t *testing.T) { os.Setenv("FOO", "def") - c, err = parseFiles([]File{ + c, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }) @@ -324,7 +323,7 @@ func TestHCLVarsWithVars(t *testing.T) { } `) - c, err := parseFiles([]File{ + c, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }) @@ -336,7 +335,7 @@ func TestHCLVarsWithVars(t *testing.T) { os.Setenv("BASE", "new") - c, err = parseFiles([]File{ + c, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }) @@ -481,7 +480,7 @@ func TestHCLMultiFileAttrs(t *testing.T) { FOO="def" `) - c, err := parseFiles([]File{ + c, err := ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }) @@ -492,7 +491,7 @@ func TestHCLMultiFileAttrs(t *testing.T) { os.Setenv("FOO", "ghi") - c, err = parseFiles([]File{ + c, err = ParseFiles([]File{ {Data: dt, Name: "c1.hcl"}, {Data: dt2, Name: "c2.hcl"}, }) diff --git a/bake/hclparser/expr.go b/bake/hclparser/expr.go new file mode 100644 index 00000000..d6ff2a03 --- /dev/null +++ b/bake/hclparser/expr.go @@ -0,0 +1,153 @@ +package hclparser + +import ( + "reflect" + "unsafe" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/pkg/errors" +) + +func funcCalls(exp hcl.Expression) ([]string, hcl.Diagnostics) { + node, ok := exp.(hclsyntax.Node) + if !ok { + fns, err := jsonFuncCallsRecursive(exp) + if err != nil { + return nil, hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid expression", + Detail: err.Error(), + Subject: exp.Range().Ptr(), + Context: exp.Range().Ptr(), + }, + } + } + return fns, nil + } + + var funcnames []string + hcldiags := hclsyntax.VisitAll(node, func(n hclsyntax.Node) hcl.Diagnostics { + if fe, ok := n.(*hclsyntax.FunctionCallExpr); ok { + funcnames = append(funcnames, fe.Name) + } + return nil + }) + if hcldiags.HasErrors() { + return nil, hcldiags + } + return funcnames, nil +} + +func jsonFuncCallsRecursive(exp hcl.Expression) ([]string, error) { + je, ok := exp.(jsonExp) + if !ok { + return nil, errors.Errorf("invalid expression type %T", exp) + } + m := map[string]struct{}{} + for _, e := range elementExpressions(je, exp) { + if err := appendJSONFuncCalls(e, m); err != nil { + return nil, err + } + } + arr := make([]string, 0, len(m)) + for n := range m { + arr = append(arr, n) + } + return arr, nil +} + +func appendJSONFuncCalls(exp hcl.Expression, m map[string]struct{}) error { + v := reflect.ValueOf(exp) + if v.Kind() != reflect.Ptr || v.IsNil() { + return errors.Errorf("invalid json expression kind %T %v", exp, v.Kind()) + } + src := v.Elem().FieldByName("src") + if src.IsZero() { + return errors.Errorf("%v has no property src", v.Elem().Type()) + } + if src.Kind() != reflect.Interface { + return errors.Errorf("%v src is not interface: %v", src.Type(), src.Kind()) + } + src = src.Elem() + if src.IsNil() { + return nil + } + if src.Kind() == reflect.Ptr { + src = src.Elem() + } + if src.Kind() != reflect.Struct { + return errors.Errorf("%v is not struct: %v", src.Type(), src.Kind()) + } + + // hcl/v2/json/ast#stringVal + val := src.FieldByName("Value") + if val.IsZero() { + return nil + } + rng := src.FieldByName("SrcRange") + if val.IsZero() { + return nil + } + var stringVal struct { + Value string + SrcRange hcl.Range + } + + if !val.Type().AssignableTo(reflect.ValueOf(stringVal.Value).Type()) { + return nil + } + if !rng.Type().AssignableTo(reflect.ValueOf(stringVal.SrcRange).Type()) { + return nil + } + // reflect.Set does not work for unexported fields + stringVal.Value = *(*string)(unsafe.Pointer(val.UnsafeAddr())) + stringVal.SrcRange = *(*hcl.Range)(unsafe.Pointer(rng.UnsafeAddr())) + + expr, diags := hclsyntax.ParseExpression([]byte(stringVal.Value), stringVal.SrcRange.Filename, stringVal.SrcRange.Start) + if diags.HasErrors() { + return nil + } + + fns, err := funcCalls(expr) + if err != nil { + return err + } + + for _, fn := range fns { + m[fn] = struct{}{} + } + + return nil +} + +type jsonExp interface { + ExprList() []hcl.Expression + ExprMap() []hcl.KeyValuePair +} + +func elementExpressions(je jsonExp, exp hcl.Expression) []hcl.Expression { + list := je.ExprList() + if len(list) != 0 { + exp := make([]hcl.Expression, 0, len(list)) + for _, e := range list { + if je, ok := e.(jsonExp); ok { + exp = append(exp, elementExpressions(je, e)...) + } + } + return exp + } + kvlist := je.ExprMap() + if len(kvlist) != 0 { + exp := make([]hcl.Expression, 0, len(kvlist)*2) + for _, p := range kvlist { + exp = append(exp, p.Key) + if je, ok := p.Value.(jsonExp); ok { + exp = append(exp, elementExpressions(je, p.Value)...) + } + } + return exp + } + return []hcl.Expression{exp} +} diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go new file mode 100644 index 00000000..43ff4e87 --- /dev/null +++ b/bake/hclparser/hclparser.go @@ -0,0 +1,474 @@ +package hclparser + +import ( + "fmt" + "math" + "math/big" + "reflect" + "strconv" + "strings" + + "github.com/docker/buildx/util/userfunc" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/pkg/errors" + "github.com/zclconf/go-cty/cty" +) + +type Opt struct { + LookupVar func(string) (string, bool) +} + +type variable struct { + Name string `json:"-" hcl:"name,label"` + Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"` + Body hcl.Body `json:"-" hcl:",body"` +} + +type functionDef struct { + Name string `json:"-" hcl:"name,label"` + Params *hcl.Attribute `json:"params,omitempty" hcl:"params"` + Variadic *hcl.Attribute `json:"variadic_param,omitempty" hcl:"variadic_params"` + Result *hcl.Attribute `json:"result,omitempty" hcl:"result"` +} + +type inputs struct { + Variables []*variable `hcl:"variable,block"` + Functions []*functionDef `hcl:"function,block"` + + Remain hcl.Body `json:"-" hcl:",remain"` +} + +type parser struct { + opt Opt + + vars map[string]*variable + attrs map[string]*hcl.Attribute + funcs map[string]*functionDef + + ectx *hcl.EvalContext + + progress map[string]struct{} + progressF map[string]struct{} + doneF map[string]struct{} +} + +func (p *parser) loadDeps(exp hcl.Expression, exclude map[string]struct{}) hcl.Diagnostics { + fns, hcldiags := funcCalls(exp) + if hcldiags.HasErrors() { + return hcldiags + } + + for _, fn := range fns { + if err := p.resolveFunction(fn); err != nil { + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid expression", + Detail: err.Error(), + Subject: exp.Range().Ptr(), + Context: exp.Range().Ptr(), + }, + } + } + } + + for _, v := range exp.Variables() { + if _, ok := exclude[v.RootName()]; ok { + continue + } + if err := p.resolveValue(v.RootName()); err != nil { + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid expression", + Detail: err.Error(), + Subject: v.SourceRange().Ptr(), + Context: v.SourceRange().Ptr(), + }, + } + } + } + + return nil +} + +func (p *parser) resolveFunction(name string) error { + if _, ok := p.doneF[name]; ok { + return nil + } + f, ok := p.funcs[name] + if !ok { + if _, ok := p.ectx.Functions[name]; ok { + return nil + } + return errors.Errorf("undefined function %s", name) + } + if _, ok := p.progressF[name]; ok { + return errors.Errorf("function cycle not allowed for %s", name) + } + p.progressF[name] = struct{}{} + + paramExprs, paramsDiags := hcl.ExprList(f.Params.Expr) + if paramsDiags.HasErrors() { + return paramsDiags + } + var diags hcl.Diagnostics + params := map[string]struct{}{} + for _, paramExpr := range paramExprs { + param := hcl.ExprAsKeyword(paramExpr) + if param == "" { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid param element", + Detail: "Each parameter name must be an identifier.", + Subject: paramExpr.Range().Ptr(), + }) + } + params[param] = struct{}{} + } + var variadic hcl.Expression + if f.Variadic != nil { + variadic = f.Variadic.Expr + param := hcl.ExprAsKeyword(variadic) + if param == "" { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid param element", + Detail: "Each parameter name must be an identifier.", + Subject: f.Variadic.Range.Ptr(), + }) + } + params[param] = struct{}{} + } + if diags.HasErrors() { + return diags + } + + if diags := p.loadDeps(f.Result.Expr, params); diags.HasErrors() { + return diags + } + + v, diags := userfunc.NewFunction(f.Params.Expr, variadic, f.Result.Expr, func() *hcl.EvalContext { + return p.ectx + }) + if diags.HasErrors() { + return diags + } + p.doneF[name] = struct{}{} + p.ectx.Functions[name] = v + + return nil +} + +func (p *parser) resolveValue(name string) (err error) { + if _, ok := p.ectx.Variables[name]; ok { + return nil + } + if _, ok := p.progress[name]; ok { + return errors.Errorf("variable cycle not allowed for %s", name) + } + p.progress[name] = struct{}{} + + var v *cty.Value + defer func() { + if v != nil { + p.ectx.Variables[name] = *v + } + }() + + def, ok := p.attrs[name] + if !ok { + vr, ok := p.vars[name] + if !ok { + return errors.Errorf("undefined variable %q", name) + } + def = vr.Default + } + + if def == nil { + val, _ := p.opt.LookupVar(name) + vv := cty.StringVal(val) + v = &vv + return + } + + if diags := p.loadDeps(def.Expr, nil); diags.HasErrors() { + return diags + } + vv, diags := def.Expr.Value(p.ectx) + if diags.HasErrors() { + return diags + } + + _, isVar := p.vars[name] + + if envv, ok := p.opt.LookupVar(name); ok && isVar { + if vv.Type().Equals(cty.Bool) { + b, err := strconv.ParseBool(envv) + if err != nil { + return errors.Wrapf(err, "failed to parse %s as bool", name) + } + vv := cty.BoolVal(b) + v = &vv + return nil + } else if vv.Type().Equals(cty.String) { + vv := cty.StringVal(envv) + v = &vv + return nil + } else if vv.Type().Equals(cty.Number) { + n, err := strconv.ParseFloat(envv, 64) + if err == nil && (math.IsNaN(n) || math.IsInf(n, 0)) { + err = errors.Errorf("invalid number value") + } + if err != nil { + return errors.Wrapf(err, "failed to parse %s as number", name) + } + vv := cty.NumberVal(big.NewFloat(n)) + v = &vv + return nil + } else { + // TODO: support lists with csv values + return errors.Errorf("unsupported type %s for variable %s", v.Type(), name) + } + } + v = &vv + return nil +} + +func Parse(b hcl.Body, opt Opt, val interface{}) hcl.Diagnostics { + reserved := map[string]struct{}{} + schema, _ := gohcl.ImpliedBodySchema(val) + + for _, bs := range schema.Blocks { + reserved[bs.Type] = struct{}{} + } + + var defs inputs + if err := gohcl.DecodeBody(b, nil, &defs); err != nil { + return err + } + + if opt.LookupVar == nil { + opt.LookupVar = func(string) (string, bool) { + return "", false + } + } + + p := &parser{ + opt: opt, + + vars: map[string]*variable{}, + attrs: map[string]*hcl.Attribute{}, + funcs: map[string]*functionDef{}, + + progress: map[string]struct{}{}, + progressF: map[string]struct{}{}, + doneF: map[string]struct{}{}, + ectx: &hcl.EvalContext{ + Variables: map[string]cty.Value{}, + Functions: stdlibFunctions, + }, + } + + for _, v := range defs.Variables { + // TODO: validate name + if _, ok := reserved[v.Name]; ok { + continue + } + p.vars[v.Name] = v + } + for _, v := range defs.Functions { + // TODO: validate name + if _, ok := reserved[v.Name]; ok { + continue + } + p.funcs[v.Name] = v + } + + attrs, diags := b.JustAttributes() + if diags.HasErrors() { + for _, d := range diags { + if d.Detail != "Blocks are not allowed here." { + return diags + } + } + } + + for _, v := range attrs { + if _, ok := reserved[v.Name]; ok { + continue + } + p.attrs[v.Name] = v + } + delete(p.attrs, "function") + + for k := range p.attrs { + if err := p.resolveValue(k); err != nil { + if diags, ok := err.(hcl.Diagnostics); ok { + return diags + } + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid attribute", + Detail: err.Error(), + Subject: &p.attrs[k].Range, + Context: &p.attrs[k].Range, + }, + } + } + } + + for k := range p.vars { + if err := p.resolveValue(k); err != nil { + if diags, ok := err.(hcl.Diagnostics); ok { + return diags + } + r := p.vars[k].Body.MissingItemRange() + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid value", + Detail: err.Error(), + Subject: &r, + Context: &r, + }, + } + } + } + + for k := range p.funcs { + if err := p.resolveFunction(k); err != nil { + if diags, ok := err.(hcl.Diagnostics); ok { + return diags + } + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid function", + Detail: err.Error(), + Subject: &p.funcs[k].Params.Range, + Context: &p.funcs[k].Params.Range, + }, + } + } + } + + content, _, diags := b.PartialContent(schema) + if diags.HasErrors() { + return diags + } + + for _, a := range content.Attributes { + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid attribute", + Detail: "global attributes currently not supported", + Subject: &a.Range, + Context: &a.Range, + }, + } + } + + m := map[string]map[string][]*hcl.Block{} + for _, b := range content.Blocks { + if len(b.Labels) == 0 || len(b.Labels) > 1 { + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid block", + Detail: fmt.Sprintf("invalid block label: %v", b.Labels), + Subject: &b.LabelRanges[0], + Context: &b.LabelRanges[0], + }, + } + } + bm, ok := m[b.Type] + if !ok { + bm = map[string][]*hcl.Block{} + m[b.Type] = bm + } + + lbl := b.Labels[0] + bm[lbl] = append(bm[lbl], b) + } + + vt := reflect.ValueOf(val).Elem().Type() + numFields := vt.NumField() + + type value struct { + reflect.Value + idx int + } + type field struct { + idx int + typ reflect.Type + values map[string]value + } + types := map[string]field{} + + for i := 0; i < numFields; i++ { + tags := strings.Split(vt.Field(i).Tag.Get("hcl"), ",") + + types[tags[0]] = field{ + idx: i, + typ: vt.Field(i).Type, + values: make(map[string]value), + } + } + + diags = hcl.Diagnostics{} + for _, b := range content.Blocks { + v := reflect.ValueOf(val) + + t, ok := types[b.Type] + if !ok { + continue + } + + vv := reflect.New(t.typ.Elem().Elem()) + diag := gohcl.DecodeBody(b.Body, p.ectx, vv.Interface()) + if diag.HasErrors() { + diags = append(diags, diag...) + continue + } + + setLabel(vv, b.Labels[0]) + + oldValue, exists := t.values[b.Labels[0]] + if exists { + if m := oldValue.Value.MethodByName("Merge"); m.IsValid() { + m.Call([]reflect.Value{vv}) + } else { + v.Elem().Field(t.idx).Index(oldValue.idx).Set(vv) + } + } else { + slice := v.Elem().Field(t.idx) + if slice.IsNil() { + slice = reflect.New(t.typ).Elem() + } + t.values[b.Labels[0]] = value{Value: vv, idx: slice.Len()} + v.Elem().Field(t.idx).Set(reflect.Append(slice, vv)) + } + } + if diags.HasErrors() { + return diags + } + + return nil +} + +func setLabel(v reflect.Value, lbl string) { + // cache field index? + numFields := v.Elem().Type().NumField() + for i := 0; i < numFields; i++ { + for _, t := range strings.Split(v.Elem().Type().Field(i).Tag.Get("hcl"), ",") { + if t == "label" { + v.Elem().Field(i).Set(reflect.ValueOf(lbl)) + return + } + } + } +} diff --git a/bake/hclparser/stdlib.go b/bake/hclparser/stdlib.go new file mode 100644 index 00000000..8ee31748 --- /dev/null +++ b/bake/hclparser/stdlib.go @@ -0,0 +1,111 @@ +package hclparser + +import ( + "github.com/hashicorp/go-cty-funcs/cidr" + "github.com/hashicorp/go-cty-funcs/crypto" + "github.com/hashicorp/go-cty-funcs/encoding" + "github.com/hashicorp/go-cty-funcs/uuid" + "github.com/hashicorp/hcl/v2/ext/tryfunc" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/function/stdlib" +) + +var stdlibFunctions = map[string]function.Function{ + "absolute": stdlib.AbsoluteFunc, + "add": stdlib.AddFunc, + "and": stdlib.AndFunc, + "base64decode": encoding.Base64DecodeFunc, + "base64encode": encoding.Base64EncodeFunc, + "bcrypt": crypto.BcryptFunc, + "byteslen": stdlib.BytesLenFunc, + "bytesslice": stdlib.BytesSliceFunc, + "can": tryfunc.CanFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "chunklist": stdlib.ChunklistFunc, + "cidrhost": cidr.HostFunc, + "cidrnetmask": cidr.NetmaskFunc, + "cidrsubnet": cidr.SubnetFunc, + "cidrsubnets": cidr.SubnetsFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "coalesce": stdlib.CoalesceFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "contains": stdlib.ContainsFunc, + "convert": typeexpr.ConvertFunc, + "distinct": stdlib.DistinctFunc, + "divide": stdlib.DivideFunc, + "element": stdlib.ElementFunc, + "equal": stdlib.EqualFunc, + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "formatdate": stdlib.FormatDateFunc, + "format": stdlib.FormatFunc, + "formatlist": stdlib.FormatListFunc, + "greaterthan": stdlib.GreaterThanFunc, + "greaterthanorequalto": stdlib.GreaterThanOrEqualToFunc, + "hasindex": stdlib.HasIndexFunc, + "indent": stdlib.IndentFunc, + "index": stdlib.IndexFunc, + "int": stdlib.IntFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "join": stdlib.JoinFunc, + "length": stdlib.LengthFunc, + "lessthan": stdlib.LessThanFunc, + "lessthanorequalto": stdlib.LessThanOrEqualToFunc, + "log": stdlib.LogFunc, + "lookup": stdlib.LookupFunc, + "lower": stdlib.LowerFunc, + "max": stdlib.MaxFunc, + "md5": crypto.Md5Func, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "modulo": stdlib.ModuloFunc, + "multiply": stdlib.MultiplyFunc, + "negate": stdlib.NegateFunc, + "notequal": stdlib.NotEqualFunc, + "not": stdlib.NotFunc, + "or": stdlib.OrFunc, + "parseint": stdlib.ParseIntFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "regexall": stdlib.RegexAllFunc, + "regex": stdlib.RegexFunc, + "regex_replace": stdlib.RegexReplaceFunc, + "reverse": stdlib.ReverseFunc, + "reverselist": stdlib.ReverseListFunc, + "rsadecrypt": crypto.RsaDecryptFunc, + "sethaselement": stdlib.SetHasElementFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setsubtract": stdlib.SetSubtractFunc, + "setsymmetricdifference": stdlib.SetSymmetricDifferenceFunc, + "setunion": stdlib.SetUnionFunc, + "sha1": crypto.Sha1Func, + "sha256": crypto.Sha256Func, + "sha512": crypto.Sha512Func, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "strlen": stdlib.StrlenFunc, + "substr": stdlib.SubstrFunc, + "subtract": stdlib.SubtractFunc, + "timeadd": stdlib.TimeAddFunc, + "title": stdlib.TitleFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "try": tryfunc.TryFunc, + "upper": stdlib.UpperFunc, + "urlencode": encoding.URLEncodeFunc, + "uuidv4": uuid.V4Func, + "uuidv5": uuid.V5Func, + "values": stdlib.ValuesFunc, + "zipmap": stdlib.ZipmapFunc, +} diff --git a/vendor/github.com/hashicorp/hcl/v2/hclparse/parser.go b/vendor/github.com/hashicorp/hcl/v2/hclparse/parser.go new file mode 100644 index 00000000..1dc2eccd --- /dev/null +++ b/vendor/github.com/hashicorp/hcl/v2/hclparse/parser.go @@ -0,0 +1,135 @@ +// Package hclparse has the main API entry point for parsing both HCL native +// syntax and HCL JSON. +// +// The main HCL package also includes SimpleParse and SimpleParseFile which +// can be a simpler interface for the common case where an application just +// needs to parse a single file. The gohcl package simplifies that further +// in its SimpleDecode function, which combines hcl.SimpleParse with decoding +// into Go struct values +// +// Package hclparse, then, is useful for applications that require more fine +// control over parsing or which need to load many separate files and keep +// track of them for possible error reporting or other analysis. +package hclparse + +import ( + "fmt" + "io/ioutil" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" +) + +// NOTE: This is the public interface for parsing. The actual parsers are +// in other packages alongside this one, with this package just wrapping them +// to provide a unified interface for the caller across all supported formats. + +// Parser is the main interface for parsing configuration files. As well as +// parsing files, a parser also retains a registry of all of the files it +// has parsed so that multiple attempts to parse the same file will return +// the same object and so the collected files can be used when printing +// diagnostics. +// +// Any diagnostics for parsing a file are only returned once on the first +// call to parse that file. Callers are expected to collect up diagnostics +// and present them together, so returning diagnostics for the same file +// multiple times would create a confusing result. +type Parser struct { + files map[string]*hcl.File +} + +// NewParser creates a new parser, ready to parse configuration files. +func NewParser() *Parser { + return &Parser{ + files: map[string]*hcl.File{}, + } +} + +// ParseHCL parses the given buffer (which is assumed to have been loaded from +// the given filename) as a native-syntax configuration file and returns the +// hcl.File object representing it. +func (p *Parser) ParseHCL(src []byte, filename string) (*hcl.File, hcl.Diagnostics) { + if existing := p.files[filename]; existing != nil { + return existing, nil + } + + file, diags := hclsyntax.ParseConfig(src, filename, hcl.Pos{Byte: 0, Line: 1, Column: 1}) + p.files[filename] = file + return file, diags +} + +// ParseHCLFile reads the given filename and parses it as a native-syntax HCL +// configuration file. An error diagnostic is returned if the given file +// cannot be read. +func (p *Parser) ParseHCLFile(filename string) (*hcl.File, hcl.Diagnostics) { + if existing := p.files[filename]; existing != nil { + return existing, nil + } + + src, err := ioutil.ReadFile(filename) + if err != nil { + return nil, hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Failed to read file", + Detail: fmt.Sprintf("The configuration file %q could not be read.", filename), + }, + } + } + + return p.ParseHCL(src, filename) +} + +// ParseJSON parses the given JSON buffer (which is assumed to have been loaded +// from the given filename) and returns the hcl.File object representing it. +func (p *Parser) ParseJSON(src []byte, filename string) (*hcl.File, hcl.Diagnostics) { + if existing := p.files[filename]; existing != nil { + return existing, nil + } + + file, diags := json.Parse(src, filename) + p.files[filename] = file + return file, diags +} + +// ParseJSONFile reads the given filename and parses it as JSON, similarly to +// ParseJSON. An error diagnostic is returned if the given file cannot be read. +func (p *Parser) ParseJSONFile(filename string) (*hcl.File, hcl.Diagnostics) { + if existing := p.files[filename]; existing != nil { + return existing, nil + } + + file, diags := json.ParseFile(filename) + p.files[filename] = file + return file, diags +} + +// AddFile allows a caller to record in a parser a file that was parsed some +// other way, thus allowing it to be included in the registry of sources. +func (p *Parser) AddFile(filename string, file *hcl.File) { + p.files[filename] = file +} + +// Sources returns a map from filenames to the raw source code that was +// read from them. This is intended to be used, for example, to print +// diagnostics with contextual information. +// +// The arrays underlying the returned slices should not be modified. +func (p *Parser) Sources() map[string][]byte { + ret := make(map[string][]byte) + for fn, f := range p.files { + ret[fn] = f.Bytes + } + return ret +} + +// Files returns a map from filenames to the File objects produced from them. +// This is intended to be used, for example, to print diagnostics with +// contextual information. +// +// The returned map and all of the objects it refers to directly or indirectly +// must not be modified. +func (p *Parser) Files() map[string]*hcl.File { + return p.files +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ecf08c8b..0539df09 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -228,6 +228,7 @@ github.com/hashicorp/hcl/v2/ext/customdecode github.com/hashicorp/hcl/v2/ext/tryfunc github.com/hashicorp/hcl/v2/ext/typeexpr github.com/hashicorp/hcl/v2/gohcl +github.com/hashicorp/hcl/v2/hclparse github.com/hashicorp/hcl/v2/hclsyntax github.com/hashicorp/hcl/v2/hclwrite github.com/hashicorp/hcl/v2/json