package bake import ( "os" "strings" "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/ext/userfunc" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" hcljson "github.com/hashicorp/hcl/v2/json" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/solver/pb" "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, } ) // Used in the first pass of decoding instead of the Config struct to disallow // interpolation while parsing variable blocks. type StaticConfig struct { Variables []*Variable `hcl:"variable,block"` Remain hcl.Body `hcl:",remain"` } 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...) } return sc } 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 } return f, &sc, nil } func ParseHCL(b hcl.Body, sc *StaticConfig) (_ *Config, err error) { // Set all variables to their default value if defined. variables := make(map[string]cty.Value) for _, variable := range sc.Variables { variables[variable.Name] = cty.StringVal(variable.Default) } userFunctions, _, diags := userfunc.DecodeUserFunctions(b, "function", func() *hcl.EvalContext { return &hcl.EvalContext{ Functions: stdlibFunctions, Variables: variables, } }) if diags.HasErrors() { return nil, diags } // Override default with values from environment. for _, env := range os.Environ() { parts := strings.SplitN(env, "=", 2) name, value := parts[0], parts[1] if _, ok := variables[name]; ok { variables[name] = cty.StringVal(value) } } functions := make(map[string]function.Function) for k, v := range stdlibFunctions { functions[k] = v } for k, v := range userFunctions { functions[k] = v } ctx := &hcl.EvalContext{ Variables: variables, Functions: functions, } var c Config // Decode with variables and functions. if err := gohcl.DecodeBody(b, ctx, &c); err != nil { return nil, err } return &c, nil } func formatHCLError(dt []byte, err error) error { if err == nil { return nil } diags, ok := err.(hcl.Diagnostics) if !ok { return err } for _, d := range diags { if d.Severity != hcl.DiagError { continue } if d.Subject != nil { src := errdefs.Source{ Info: &pb.SourceInfo{ Filename: d.Subject.Filename, Data: dt, }, Ranges: []*pb.Range{toErrRange(d.Subject)}, } err = errdefs.WithSource(err, src) break } } return err } func toErrRange(in *hcl.Range) *pb.Range { return &pb.Range{ Start: pb.Position{Line: int32(in.Start.Line), Character: int32(in.Start.Column)}, End: pb.Position{Line: int32(in.End.Line), Character: int32(in.End.Column)}, } }