// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package lang import ( "crypto/sha256" "fmt" "io" "log" "sync" "github.com/hashicorp/terraform/internal/addrs" "github.com/zclconf/go-cty/cty" ) type priorResultHash struct { hash [sha256.Size]byte // when the result was from a current run, we keep a record of the result // value to aid in debugging. Results stored in the plan will only have the // hash to avoid bloating the plan with what could be many very large // values. value cty.Value } type FunctionResults struct { mu sync.Mutex // results stores the prior result from a function call, keyed by // the hash of the function name and arguments. results map[[sha256.Size]byte]priorResultHash } // NewFunctionResultsTable initializes a mapping of function calls to prior // results used to validate function calls. The hashes argument is an // optional slice of prior result hashes used to preload the cache. func NewFunctionResultsTable(hashes []FunctionResultHash) *FunctionResults { res := &FunctionResults{ results: make(map[[sha256.Size]byte]priorResultHash), } res.insertHashes(hashes) return res } // CheckPrior compares the function call against any cached results, and returns // an error if the result does not match a prior call. func (f *FunctionResults) CheckPrior(name string, args []cty.Value, result cty.Value) error { return f.CheckPriorProvider(addrs.Provider{}, name, args, result) } // CheckPriorProvider compares the provider function call against any cached // results, and returns an error if the result does not match a prior call. func (f *FunctionResults) CheckPriorProvider(provider addrs.Provider, name string, args []cty.Value, result cty.Value) error { // Don't cache unknown values. We could technically store types and // refinements for validation, but we don't currently have a way to // serialize those in the plan. Unknowns are also handled much more // gracefully throughout the evaluation system, whereas invalid data is // harder to trace back to the source since it's usually only visible due to // unexpected side-effects. if !result.IsKnown() { return nil } argSum := sha256.New() if !provider.IsZero() { io.WriteString(argSum, provider.String()+"|") } io.WriteString(argSum, name) for _, arg := range args { // cty.Values have a Hash method, but it is not collision resistant. We // are going to rely on the GoString formatting instead, which gives // detailed results for all values. io.WriteString(argSum, "|"+arg.GoString()) } f.mu.Lock() defer f.mu.Unlock() argHash := [sha256.Size]byte(argSum.Sum(nil)) resHash := sha256.Sum256([]byte(result.GoString())) res, ok := f.results[argHash] if !ok { f.results[argHash] = priorResultHash{ hash: resHash, value: result, } return nil } if resHash != res.hash { provPrefix := "" if !provider.IsZero() { provPrefix = fmt.Sprintf("provider %s ", provider) } // Log the args for debugging in case the hcl context is // insufficient. The error should be adequate most of the time, and // could already be quite long, so we don't want to add all // arguments too. log.Printf("[ERROR] %sfunction %s returned an inconsistent result with args: %#v\n", provPrefix, name, args) // The hcl package will add the necessary context around the error in // the diagnostic, but we add the differing results when we can. if res.value != cty.NilVal { return fmt.Errorf("function returned an inconsistent result,\nwas: %#v,\nnow: %#v", res.value, result) } return fmt.Errorf("function returned an inconsistent result") } return nil } // insertHashes insert key-value pairs to the functionResults map. This is used // to preload stored values before any Verify calls are made. func (f *FunctionResults) insertHashes(hashes []FunctionResultHash) { f.mu.Lock() defer f.mu.Unlock() for _, res := range hashes { f.results[[sha256.Size]byte(res.Key)] = priorResultHash{ hash: [sha256.Size]byte(res.Result), } } } // FunctionResultHash contains the key and result hash values from a prior function // call. type FunctionResultHash struct { Key []byte Result []byte } // copy the hash values into a struct which can be recorded in the plan. func (f *FunctionResults) GetHashes() []FunctionResultHash { f.mu.Lock() defer f.mu.Unlock() var res []FunctionResultHash for k, r := range f.results { res = append(res, FunctionResultHash{Key: k[:], Result: r.hash[:]}) } return res }