diff --git a/internal/lang/funcs/crypto.go b/internal/lang/funcs/crypto.go index f948a1c1d4..359d0316de 100644 --- a/internal/lang/funcs/crypto.go +++ b/internal/lang/funcs/crypto.go @@ -80,8 +80,8 @@ var Base64Sha256Func = makeStringHashFunction(sha256.New, base64.StdEncoding.Enc // MakeFileBase64Sha256Func constructs a function that is like Base64Sha256Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileBase64Sha256Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha256.New, base64.StdEncoding.EncodeToString) +func MakeFileBase64Sha256Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha256.New, base64.StdEncoding.EncodeToString, wrap) } // Base64Sha512Func constructs a function that computes the SHA256 hash of a given string @@ -90,8 +90,8 @@ var Base64Sha512Func = makeStringHashFunction(sha512.New, base64.StdEncoding.Enc // MakeFileBase64Sha512Func constructs a function that is like Base64Sha512Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileBase64Sha512Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha512.New, base64.StdEncoding.EncodeToString) +func MakeFileBase64Sha512Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha512.New, base64.StdEncoding.EncodeToString, wrap) } // BcryptFunc constructs a function that computes a hash of the given string using the Blowfish cipher. @@ -138,8 +138,8 @@ var Md5Func = makeStringHashFunction(md5.New, hex.EncodeToString) // MakeFileMd5Func constructs a function that is like Md5Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileMd5Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, md5.New, hex.EncodeToString) +func MakeFileMd5Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, md5.New, hex.EncodeToString, wrap) } // RsaDecryptFunc constructs a function that decrypts an RSA-encrypted ciphertext. @@ -198,8 +198,8 @@ var Sha1Func = makeStringHashFunction(sha1.New, hex.EncodeToString) // MakeFileSha1Func constructs a function that is like Sha1Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileSha1Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha1.New, hex.EncodeToString) +func MakeFileSha1Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha1.New, hex.EncodeToString, wrap) } // Sha256Func contructs a function that computes the SHA256 hash of a given string @@ -208,8 +208,8 @@ var Sha256Func = makeStringHashFunction(sha256.New, hex.EncodeToString) // MakeFileSha256Func constructs a function that is like Sha256Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileSha256Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha256.New, hex.EncodeToString) +func MakeFileSha256Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha256.New, hex.EncodeToString, wrap) } // Sha512Func contructs a function that computes the SHA512 hash of a given string @@ -218,8 +218,8 @@ var Sha512Func = makeStringHashFunction(sha512.New, hex.EncodeToString) // MakeFileSha512Func constructs a function that is like Sha512Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileSha512Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha512.New, hex.EncodeToString) +func MakeFileSha512Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha512.New, hex.EncodeToString, wrap) } func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) function.Function { @@ -242,7 +242,7 @@ func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) functi }) } -func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) string) function.Function { +func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) string, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -252,7 +252,7 @@ func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) }, Type: function.StaticReturnType(cty.String), RefineResult: refineNotNull, - Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + Impl: wrap(func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { path := args[0].AsString() f, err := openFile(baseDir, path) if err != nil { @@ -267,7 +267,7 @@ func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) } rv := enc(h.Sum(nil)) return cty.StringVal(rv), nil - }, + }), }) } diff --git a/internal/lang/funcs/crypto_test.go b/internal/lang/funcs/crypto_test.go index efabb95c72..c7a14bb743 100644 --- a/internal/lang/funcs/crypto_test.go +++ b/internal/lang/funcs/crypto_test.go @@ -147,7 +147,7 @@ func TestFileBase64Sha256(t *testing.T) { }, } - fileSHA256 := MakeFileBase64Sha256Func(".") + fileSHA256 := MakeFileBase64Sha256Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filebase64sha256(%#v)", test.Path), func(t *testing.T) { @@ -228,7 +228,7 @@ func TestFileBase64Sha512(t *testing.T) { }, } - fileSHA512 := MakeFileBase64Sha512Func(".") + fileSHA512 := MakeFileBase64Sha512Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filebase64sha512(%#v)", test.Path), func(t *testing.T) { @@ -346,7 +346,7 @@ func TestFileMD5(t *testing.T) { }, } - fileMD5 := MakeFileMd5Func(".") + fileMD5 := MakeFileMd5Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filemd5(%#v)", test.Path), func(t *testing.T) { @@ -503,7 +503,7 @@ func TestFileSHA1(t *testing.T) { }, } - fileSHA1 := MakeFileSha1Func(".") + fileSHA1 := MakeFileSha1Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filesha1(%#v)", test.Path), func(t *testing.T) { @@ -581,7 +581,7 @@ func TestFileSHA256(t *testing.T) { }, } - fileSHA256 := MakeFileSha256Func(".") + fileSHA256 := MakeFileSha256Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filesha256(%#v)", test.Path), func(t *testing.T) { @@ -659,7 +659,7 @@ func TestFileSHA512(t *testing.T) { }, } - fileSHA512 := MakeFileSha512Func(".") + fileSHA512 := MakeFileSha512Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filesha512(%#v)", test.Path), func(t *testing.T) { diff --git a/internal/lang/funcs/filesystem.go b/internal/lang/funcs/filesystem.go index 07a978618f..02ddcc6923 100644 --- a/internal/lang/funcs/filesystem.go +++ b/internal/lang/funcs/filesystem.go @@ -24,7 +24,7 @@ import ( // MakeFileFunc constructs a function that takes a file path and returns the // contents of that file, either directly as a string (where valid UTF-8 is // required) or as a string containing base64 bytes. -func MakeFileFunc(baseDir string, encBase64 bool) function.Function { +func MakeFileFunc(baseDir string, encBase64 bool, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -36,7 +36,7 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { }, Type: function.StaticReturnType(cty.String), RefineResult: refineNotNull, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() if !pathArg.IsKnown() { @@ -60,7 +60,7 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { } return cty.StringVal(string(src)).WithMarks(pathMarks), nil } - }, + }), }) } @@ -77,7 +77,7 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { // As a special exception, a referenced template file may not recursively call // the templatefile function, since that would risk the same file being // included into itself indefinitely. -func MakeTemplateFileFunc(baseDir string, funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string])) function.Function { +func MakeTemplateFileFunc(baseDir string, funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]), wrap ImplWrapper) function.Function { loadTmpl := func(fn string, marks cty.ValueMarks) (hcl.Expression, cty.ValueMarks, error) { // We re-use File here to ensure the same filename interpretation // as it does, along with its other safety checks. @@ -133,7 +133,7 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() (funcs map[string]funct val, err := renderTmpl(expr, vars) return val.Type(), err }, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() vars, varsMarks := args[1].UnmarkDeep() @@ -148,14 +148,13 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() (funcs map[string]funct } result, err := renderTmpl(expr, vars) return result.WithMarks(tmplMarks, varsMarks), err - }, + }), }) - } // MakeFileExistsFunc constructs a function that takes a path // and determines whether a file exists at that path -func MakeFileExistsFunc(baseDir string) function.Function { +func MakeFileExistsFunc(baseDir string, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -167,7 +166,7 @@ func MakeFileExistsFunc(baseDir string) function.Function { }, Type: function.StaticReturnType(cty.Bool), RefineResult: refineNotNull, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() if !pathArg.IsKnown() { @@ -223,13 +222,13 @@ func MakeFileExistsFunc(baseDir string) function.Function { } return cty.False, err - }, + }), }) } // MakeFileSetFunc constructs a function that takes a glob pattern // and enumerates a file set from that pattern -func MakeFileSetFunc(baseDir string) function.Function { +func MakeFileSetFunc(baseDir string, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -247,7 +246,7 @@ func MakeFileSetFunc(baseDir string) function.Function { }, Type: function.StaticReturnType(cty.Set(cty.String)), RefineResult: refineNotNull, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() patternArg, patternMarks := args[1].Unmark() @@ -304,7 +303,7 @@ func MakeFileSetFunc(baseDir string) function.Function { } return cty.SetVal(matchVals).WithMarks(marks...), nil - }, + }), }) } @@ -416,7 +415,7 @@ func readFileBytes(baseDir, path string, marks cty.ValueMarks) ([]byte, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func File(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileFunc(baseDir, false) + fn := MakeFileFunc(baseDir, false, noopWrapper) return fn.Call([]cty.Value{path}) } @@ -426,7 +425,7 @@ func File(baseDir string, path cty.Value) (cty.Value, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileExists(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileExistsFunc(baseDir) + fn := MakeFileExistsFunc(baseDir, noopWrapper) return fn.Call([]cty.Value{path}) } @@ -436,7 +435,7 @@ func FileExists(baseDir string, path cty.Value) (cty.Value, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) { - fn := MakeFileSetFunc(baseDir) + fn := MakeFileSetFunc(baseDir, noopWrapper) return fn.Call([]cty.Value{path, pattern}) } @@ -448,7 +447,7 @@ func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileBase64(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileFunc(baseDir, true) + fn := MakeFileFunc(baseDir, true, noopWrapper) return fn.Call([]cty.Value{path}) } @@ -482,3 +481,12 @@ func Dirname(path cty.Value) (cty.Value, error) { func Pathexpand(path cty.Value) (cty.Value, error) { return PathExpandFunc.Call([]cty.Value{path}) } + +// ImplWrapper allows us to pass in a wrapper function to inject behavior into +// function implementations, because we don't have access to the function.Spec +// from the returned function.Function +type ImplWrapper func(function.ImplFunc) function.ImplFunc + +func noopWrapper(fn function.ImplFunc) function.ImplFunc { + return fn +} diff --git a/internal/lang/funcs/filesystem_test.go b/internal/lang/funcs/filesystem_test.go index 51b49abb11..8291ef9790 100644 --- a/internal/lang/funcs/filesystem_test.go +++ b/internal/lang/funcs/filesystem_test.go @@ -251,7 +251,7 @@ func TestTemplateFile(t *testing.T) { funcsFunc := func() (funcTable map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]) { return funcs, collections.NewSetCmp[string](), collections.NewSetCmp[string]("templatefile") } - templateFileFn := MakeTemplateFileFunc(".", funcsFunc) + templateFileFn := MakeTemplateFileFunc(".", funcsFunc, noopWrapper) funcs["templatefile"] = templateFileFn funcs["core::templatefile"] = templateFileFn diff --git a/internal/lang/functions.go b/internal/lang/functions.go index 36d242ce7c..7f944a32a6 100644 --- a/internal/lang/functions.go +++ b/internal/lang/functions.go @@ -91,16 +91,16 @@ func (s *Scope) Functions() map[string]function.Function { "endswith": funcs.EndsWithFunc, "ephemeralasnull": funcs.EphemeralAsNullFunc, "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), + "file": funcs.MakeFileFunc(s.BaseDir, false, immutableResults("file", s.FunctionResults)), + "fileexists": funcs.MakeFileExistsFunc(s.BaseDir, immutableResults("fileexists", s.FunctionResults)), + "fileset": funcs.MakeFileSetFunc(s.BaseDir, immutableResults("fileset", s.FunctionResults)), + "filebase64": funcs.MakeFileFunc(s.BaseDir, true, immutableResults("filebase64", s.FunctionResults)), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir, immutableResults("filebase64sha256", s.FunctionResults)), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir, immutableResults("filebase64sha512", s.FunctionResults)), + "filemd5": funcs.MakeFileMd5Func(s.BaseDir, immutableResults("filemd5", s.FunctionResults)), + "filesha1": funcs.MakeFileSha1Func(s.BaseDir, immutableResults("filesha1", s.FunctionResults)), + "filesha256": funcs.MakeFileSha256Func(s.BaseDir, immutableResults("filesha256", s.FunctionResults)), + "filesha512": funcs.MakeFileSha512Func(s.BaseDir, immutableResults("filesha512", s.FunctionResults)), "flatten": stdlib.FlattenFunc, "floor": stdlib.FloorFunc, "format": stdlib.FormatFunc, @@ -190,7 +190,7 @@ func (s *Scope) Functions() map[string]function.Function { // overwriting the relevant entries. return s.funcs, filesystemFunctions, templateFunctions } - coreFuncs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, funcsFunc) + coreFuncs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, funcsFunc, immutableResults("templatefile", s.FunctionResults)) coreFuncs["templatestring"] = funcs.MakeTemplateStringFunc(funcsFunc) if s.ConsoleMode { @@ -301,16 +301,16 @@ func baseFunctions(baseDir string) map[string]function.Function { "element": stdlib.ElementFunc, "endswith": funcs.EndsWithFunc, "chunklist": stdlib.ChunklistFunc, - "file": funcs.MakeFileFunc(baseDir, false), - "fileexists": funcs.MakeFileExistsFunc(baseDir), - "fileset": funcs.MakeFileSetFunc(baseDir), - "filebase64": funcs.MakeFileFunc(baseDir, true), - "filebase64sha256": funcs.MakeFileBase64Sha256Func(baseDir), - "filebase64sha512": funcs.MakeFileBase64Sha512Func(baseDir), - "filemd5": funcs.MakeFileMd5Func(baseDir), - "filesha1": funcs.MakeFileSha1Func(baseDir), - "filesha256": funcs.MakeFileSha256Func(baseDir), - "filesha512": funcs.MakeFileSha512Func(baseDir), + "file": funcs.MakeFileFunc(baseDir, false, noopWrapper), + "fileexists": funcs.MakeFileExistsFunc(baseDir, noopWrapper), + "fileset": funcs.MakeFileSetFunc(baseDir, noopWrapper), + "filebase64": funcs.MakeFileFunc(baseDir, true, noopWrapper), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(baseDir, noopWrapper), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(baseDir, noopWrapper), + "filemd5": funcs.MakeFileMd5Func(baseDir, noopWrapper), + "filesha1": funcs.MakeFileSha1Func(baseDir, noopWrapper), + "filesha256": funcs.MakeFileSha256Func(baseDir, noopWrapper), + "filesha512": funcs.MakeFileSha512Func(baseDir, noopWrapper), "flatten": stdlib.FlattenFunc, "floor": stdlib.FloorFunc, "format": stdlib.FormatFunc, @@ -394,7 +394,7 @@ func baseFunctions(baseDir string) map[string]function.Function { // The templatefile function prevents recursive calls to itself // by copying this map and overwriting the "templatefile" entry. return fs, filesystemFunctions, templateFunctions - }) + }, noopWrapper) return fs } @@ -441,3 +441,33 @@ func (s *Scope) experimentalFunction(experiment experiments.Experiment, fn funct type ExternalFuncs struct { Provider map[string]map[string]function.Function } + +// immutableResults is a wrapper for cty function implementations which may +// otherwise not return consistent results because they depends on data outside +// of Terraform. Due to the fact that the cty functions are a concrete type, and +// the implementation is hidden within a private struct field, we need to pass +// along these closures to get the data to the actual call site. +func immutableResults(name string, priorResults *FunctionResults) func(fn function.ImplFunc) function.ImplFunc { + if priorResults == nil { + return func(fn function.ImplFunc) function.ImplFunc { + return fn + } + } + return func(fn function.ImplFunc) function.ImplFunc { + return func(args []cty.Value, retType cty.Type) (cty.Value, error) { + res, err := fn(args, retType) + if err != nil { + return res, err + } + err = priorResults.CheckPrior(name, args, res) + if err != nil { + return cty.UnknownVal(retType), err + } + return res, err + } + } +} + +func noopWrapper(fn function.ImplFunc) function.ImplFunc { + return fn +}