diff --git a/internal/lang/funcs/filesystem.go b/internal/lang/funcs/filesystem.go index e0b394c056..5447d698eb 100644 --- a/internal/lang/funcs/filesystem.go +++ b/internal/lang/funcs/filesystem.go @@ -135,7 +135,7 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems funcs := make(map[string]function.Function, len(givenFuncs)) for name, fn := range givenFuncs { - if name == "templatefile" { + if name == "templatefile" || name == "core::templatefile" { // We stub this one out to prevent recursive calls. funcs[name] = function.New(&function.Spec{ Params: params, diff --git a/internal/lang/funcs/filesystem_test.go b/internal/lang/funcs/filesystem_test.go index f003ac3c0e..e54eae6ce3 100644 --- a/internal/lang/funcs/filesystem_test.go +++ b/internal/lang/funcs/filesystem_test.go @@ -151,6 +151,12 @@ func TestTemplateFile(t *testing.T) { cty.NilVal, `testdata/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside templatefile call.`, }, + { + cty.StringVal("testdata/recursive_namespaced.tmpl"), + cty.MapValEmpty(cty.String), + cty.NilVal, + `testdata/recursive_namespaced.tmpl:1,3-22: Error in function call; Call to function "core::templatefile" failed: cannot recursively call templatefile from inside templatefile call.`, + }, { cty.StringVal("testdata/list.tmpl"), cty.ObjectVal(map[string]cty.Value{ @@ -183,8 +189,10 @@ func TestTemplateFile(t *testing.T) { templateFileFn := MakeTemplateFileFunc(".", func() map[string]function.Function { return map[string]function.Function{ - "join": stdlib.JoinFunc, - "templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this + "join": stdlib.JoinFunc, + "core::join": stdlib.JoinFunc, + "templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this + "core::templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this } }) diff --git a/internal/lang/funcs/testdata/recursive_namespaced.tmpl b/internal/lang/funcs/testdata/recursive_namespaced.tmpl new file mode 100644 index 0000000000..f346bfdaab --- /dev/null +++ b/internal/lang/funcs/testdata/recursive_namespaced.tmpl @@ -0,0 +1 @@ +${core::templatefile("recursive_namespaced.tmpl", {})} \ No newline at end of file diff --git a/internal/lang/functions.go b/internal/lang/functions.go index 25db769df7..3c4670b195 100644 --- a/internal/lang/functions.go +++ b/internal/lang/functions.go @@ -29,34 +29,157 @@ func (s *Scope) Functions() map[string]function.Function { if s.funcs == nil { s.funcs = baseFunctions(s.BaseDir) - // Then we add some functions that are only relevant when being accessed - // from inside a specific scope. + coreFuncs := map[string]function.Function{ + "abs": stdlib.AbsoluteFunc, + "abspath": funcs.AbsPathFunc, + "alltrue": funcs.AllTrueFunc, + "anytrue": funcs.AnyTrueFunc, + "basename": funcs.BasenameFunc, + "base64decode": funcs.Base64DecodeFunc, + "base64encode": funcs.Base64EncodeFunc, + "base64gzip": funcs.Base64GzipFunc, + "base64sha256": funcs.Base64Sha256Func, + "base64sha512": funcs.Base64Sha512Func, + "bcrypt": funcs.BcryptFunc, + "can": tryfunc.CanFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "cidrhost": funcs.CidrHostFunc, + "cidrnetmask": funcs.CidrNetmaskFunc, + "cidrsubnet": funcs.CidrSubnetFunc, + "cidrsubnets": funcs.CidrSubnetsFunc, + "coalesce": funcs.CoalesceFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "contains": stdlib.ContainsFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "dirname": funcs.DirnameFunc, + "distinct": stdlib.DistinctFunc, + "element": stdlib.ElementFunc, + "endswith": funcs.EndsWithFunc, + "chunklist": stdlib.ChunklistFunc, + "file": funcs.MakeFileFunc(s.BaseDir, false), + "fileexists": funcs.MakeFileExistsFunc(s.BaseDir), + "fileset": funcs.MakeFileSetFunc(s.BaseDir), + "filebase64": funcs.MakeFileFunc(s.BaseDir, true), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir), + "filemd5": funcs.MakeFileMd5Func(s.BaseDir), + "filesha1": funcs.MakeFileSha1Func(s.BaseDir), + "filesha256": funcs.MakeFileSha256Func(s.BaseDir), + "filesha512": funcs.MakeFileSha512Func(s.BaseDir), + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "format": stdlib.FormatFunc, + "formatdate": stdlib.FormatDateFunc, + "formatlist": stdlib.FormatListFunc, + "indent": stdlib.IndentFunc, + "index": funcs.IndexFunc, // stdlib.IndexFunc is not compatible + "join": stdlib.JoinFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "length": funcs.LengthFunc, + "list": funcs.ListFunc, + "log": stdlib.LogFunc, + "lookup": funcs.LookupFunc, + "lower": stdlib.LowerFunc, + "map": funcs.MapFunc, + "matchkeys": funcs.MatchkeysFunc, + "max": stdlib.MaxFunc, + "md5": funcs.Md5Func, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "one": funcs.OneFunc, + "parseint": stdlib.ParseIntFunc, + "pathexpand": funcs.PathExpandFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "regex": stdlib.RegexFunc, + "regexall": stdlib.RegexAllFunc, + "replace": funcs.ReplaceFunc, + "reverse": stdlib.ReverseListFunc, + "rsadecrypt": funcs.RsaDecryptFunc, + "sensitive": funcs.SensitiveFunc, + "nonsensitive": funcs.NonsensitiveFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setsubtract": stdlib.SetSubtractFunc, + "setunion": stdlib.SetUnionFunc, + "sha1": funcs.Sha1Func, + "sha256": funcs.Sha256Func, + "sha512": funcs.Sha512Func, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "startswith": funcs.StartsWithFunc, + "strcontains": funcs.StrContainsFunc, + "strrev": stdlib.ReverseFunc, + "substr": stdlib.SubstrFunc, + "sum": funcs.SumFunc, + "textdecodebase64": funcs.TextDecodeBase64Func, + "textencodebase64": funcs.TextEncodeBase64Func, + "timestamp": funcs.TimestampFunc, + "timeadd": stdlib.TimeAddFunc, + "timecmp": funcs.TimeCmpFunc, + "title": stdlib.TitleFunc, + "tostring": funcs.MakeToFunc(cty.String), + "tonumber": funcs.MakeToFunc(cty.Number), + "tobool": funcs.MakeToFunc(cty.Bool), + "toset": funcs.MakeToFunc(cty.Set(cty.DynamicPseudoType)), + "tolist": funcs.MakeToFunc(cty.List(cty.DynamicPseudoType)), + "tomap": funcs.MakeToFunc(cty.Map(cty.DynamicPseudoType)), + "transpose": funcs.TransposeFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "try": tryfunc.TryFunc, + "upper": stdlib.UpperFunc, + "urlencode": funcs.URLEncodeFunc, + "uuid": funcs.UUIDFunc, + "uuidv5": funcs.UUIDV5Func, + "values": stdlib.ValuesFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, + "zipmap": stdlib.ZipmapFunc, + } + + coreFuncs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, func() map[string]function.Function { + // The templatefile function prevents recursive calls to itself + // by copying this map and overwriting the "templatefile" and + // "core:templatefile" entries. + return s.funcs + }) if s.ConsoleMode { // The type function is only available in terraform console. - s.funcs["type"] = funcs.TypeFunc + coreFuncs["type"] = funcs.TypeFunc } if !s.ConsoleMode { // The plantimestamp function doesn't make sense in the terraform // console. - s.funcs["plantimestamp"] = funcs.MakeStaticTimestampFunc(s.PlanTimestamp) + coreFuncs["plantimestamp"] = funcs.MakeStaticTimestampFunc(s.PlanTimestamp) } if s.PureOnly { // Force our few impure functions to return unknown so that we // can defer evaluating them until a later pass. for _, name := range impureFunctions { - s.funcs[name] = function.Unpredictable(s.funcs[name]) + coreFuncs[name] = function.Unpredictable(coreFuncs[name]) } } - // Add a description to each function and parameter based on the - // contents of descriptionList. - // One must create a matching description entry whenever a new - // function is introduced. - for name, f := range s.funcs { - s.funcs[name] = funcs.WithDescription(name, f) + // All of the built-in functions are also available under the "core::" + // namespace, to distinguish from the "provider::" and "module::" + // namespaces that can serve as external extension points. + s.funcs = make(map[string]function.Function, len(coreFuncs)*2) + for name, fn := range coreFuncs { + s.funcs[name] = funcs.WithDescription(name, fn) + s.funcs["core::"+name] = fn } } s.funcsLock.Unlock() diff --git a/internal/lang/functions_test.go b/internal/lang/functions_test.go index c1746d92ef..acb6ce6f18 100644 --- a/internal/lang/functions_test.go +++ b/internal/lang/functions_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "time" @@ -960,6 +961,10 @@ func TestFunctions(t *testing.T) { `templatefile("hello.tmpl", {name = "Jodie"})`, cty.StringVal("Hello, Jodie!"), }, + { + `core::templatefile("hello.tmpl", {name = "Namespaced Jodie"})`, + cty.StringVal("Hello, Namespaced Jodie!"), + }, }, "timeadd": { @@ -1100,6 +1105,10 @@ func TestFunctions(t *testing.T) { `upper("hello")`, cty.StringVal("HELLO"), }, + { + `core::upper("hello")`, + cty.StringVal("HELLO"), + }, }, "urlencode": { @@ -1207,6 +1216,11 @@ func TestFunctions(t *testing.T) { delete(allFunctions, impureFunc) } for f := range scope.Functions() { + if strings.Contains(f, "::") { + // Only non-namespaced functions are absolutely required to + // have at least one test. (Others _may_ have tests.) + continue + } if _, ok := tests[f]; !ok { t.Errorf("Missing test for function %s\n", f) }