Skip to content

Commit d016070

Browse files
committed
move function results hashing to lang
We need to abstract the function results verification to use internally too, so start by moving it out of the providers code.
1 parent 49e8b56 commit d016070

File tree

17 files changed

+168
-146
lines changed

17 files changed

+168
-146
lines changed

internal/lang/function_results.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package lang
5+
6+
import (
7+
"crypto/sha256"
8+
"fmt"
9+
"io"
10+
"log"
11+
"sync"
12+
13+
"github.com/hashicorp/terraform/internal/addrs"
14+
"github.com/zclconf/go-cty/cty"
15+
)
16+
17+
type priorResult struct {
18+
hash [sha256.Size]byte
19+
// when the result was from a current run, we keep a record of the result
20+
// value to aid in debugging. Results stored in the plan will only have the
21+
// hash to avoid bloating the plan with what could be many very large
22+
// values.
23+
value cty.Value
24+
}
25+
26+
type FunctionResults struct {
27+
mu sync.Mutex
28+
// results stores the prior result from a function call, keyed by
29+
// the hash of the function name and arguments.
30+
results map[[sha256.Size]byte]priorResult
31+
}
32+
33+
// NewFunctionResultsTable initializes a mapping of function calls to prior
34+
// results used to validate function calls. The hashes argument is an
35+
// optional slice of prior result hashes used to preload the cache.
36+
func NewFunctionResultsTable(hashes []FunctionHash) *FunctionResults {
37+
res := &FunctionResults{
38+
results: make(map[[sha256.Size]byte]priorResult),
39+
}
40+
41+
res.insertHashes(hashes)
42+
return res
43+
}
44+
45+
// CheckPrior compares the function call against any cached results, and returns
46+
// an error if the result does not match a prior call. A zero-value provider
47+
// address can be used for internal functions which need this validation.
48+
func (f *FunctionResults) CheckPrior(provider addrs.Provider, name string, args []cty.Value, result cty.Value) error {
49+
argSum := sha256.New()
50+
51+
if !provider.IsZero() {
52+
io.WriteString(argSum, provider.String()+"|")
53+
}
54+
io.WriteString(argSum, name)
55+
56+
for _, arg := range args {
57+
// cty.Values have a Hash method, but it is not collision resistant. We
58+
// are going to rely on the GoString formatting instead, which gives
59+
// detailed results for all values.
60+
io.WriteString(argSum, "|"+arg.GoString())
61+
}
62+
63+
f.mu.Lock()
64+
defer f.mu.Unlock()
65+
66+
argHash := [sha256.Size]byte(argSum.Sum(nil))
67+
resHash := sha256.Sum256([]byte(result.GoString()))
68+
69+
res, ok := f.results[argHash]
70+
if !ok {
71+
f.results[argHash] = priorResult{
72+
hash: resHash,
73+
value: result,
74+
}
75+
return nil
76+
}
77+
78+
if resHash != res.hash {
79+
provPrefix := ""
80+
if !provider.IsZero() {
81+
provPrefix = fmt.Sprintf("provider %s ", provider)
82+
}
83+
// Log the args for debugging in case the hcl context is
84+
// insufficient. The error should be adequate most of the time, and
85+
// could already be quite long, so we don't want to add all
86+
// arguments too.
87+
log.Printf("[ERROR] %sfunction %s returned an inconsistent result with args: %#v\n", provPrefix, name, args)
88+
// The hcl package will add the necessary context around the error in
89+
// the diagnostic, but we add the differing results when we can.
90+
if res.value != cty.NilVal {
91+
return fmt.Errorf("function returned an inconsistent result,\nwas: %#v,\nnow: %#v", res.value, result)
92+
}
93+
return fmt.Errorf("function returned an inconsistent result")
94+
}
95+
96+
return nil
97+
}
98+
99+
// insertHashes insert key-value pairs to the functionResults map. This is used
100+
// to preload stored values before any Verify calls are made.
101+
func (f *FunctionResults) insertHashes(hashes []FunctionHash) {
102+
f.mu.Lock()
103+
defer f.mu.Unlock()
104+
105+
for _, res := range hashes {
106+
f.results[[sha256.Size]byte(res.Key)] = priorResult{
107+
hash: [sha256.Size]byte(res.Result),
108+
}
109+
}
110+
}
111+
112+
// FunctionHash contains the key and result hash values from a prior function
113+
// call.
114+
type FunctionHash struct {
115+
Key []byte
116+
Result []byte
117+
}
118+
119+
// copy the hash values into a struct which can be recorded in the plan.
120+
func (f *FunctionResults) GetHashes() []FunctionHash {
121+
f.mu.Lock()
122+
defer f.mu.Unlock()
123+
124+
var res []FunctionHash
125+
for k, r := range f.results {
126+
res = append(res, FunctionHash{Key: k[:], Result: r.hash[:]})
127+
}
128+
return res
129+
}

internal/providers/functions_test.go renamed to internal/lang/function_results_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) HashiCorp, Inc.
22
// SPDX-License-Identifier: BUSL-1.1
33

4-
package providers
4+
package lang
55

66
import (
77
"fmt"
@@ -162,12 +162,12 @@ func TestFunctionCache(t *testing.T) {
162162
for i, test := range tests {
163163
t.Run(fmt.Sprint(i), func(t *testing.T) {
164164
results := NewFunctionResultsTable(nil)
165-
err := results.checkPrior(test.first.provider, test.first.name, test.first.args, test.first.result)
165+
err := results.CheckPrior(test.first.provider, test.first.name, test.first.args, test.first.result)
166166
if err != nil {
167167
t.Fatal("error on first call!", err)
168168
}
169169

170-
err = results.checkPrior(test.second.provider, test.second.name, test.second.args, test.second.result)
170+
err = results.CheckPrior(test.second.provider, test.second.name, test.second.args, test.second.result)
171171

172172
if err != nil && !test.expectErr {
173173
t.Fatal(err)
@@ -177,7 +177,7 @@ func TestFunctionCache(t *testing.T) {
177177
newResults := NewFunctionResultsTable(results.GetHashes())
178178

179179
originalErr := err != nil
180-
reloadedErr := newResults.checkPrior(test.second.provider, test.second.name, test.second.args, test.second.result) != nil
180+
reloadedErr := newResults.CheckPrior(test.second.provider, test.second.name, test.second.args, test.second.result) != nil
181181

182182
if originalErr != reloadedErr {
183183
t.Fatalf("original check returned err:%t, reloaded check returned err:%t", originalErr, reloadedErr)

internal/plans/plan.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import (
1212
"github.com/hashicorp/terraform/internal/addrs"
1313
"github.com/hashicorp/terraform/internal/collections"
1414
"github.com/hashicorp/terraform/internal/configs/configschema"
15+
"github.com/hashicorp/terraform/internal/lang"
1516
"github.com/hashicorp/terraform/internal/lang/globalref"
1617
"github.com/hashicorp/terraform/internal/moduletest/mocking"
17-
"github.com/hashicorp/terraform/internal/providers"
1818
"github.com/hashicorp/terraform/internal/states"
1919
)
2020

@@ -162,7 +162,7 @@ type Plan struct {
162162
// ProviderFunctionResults stores hashed results from all provider
163163
// function calls, so that calls during apply can be checked for
164164
// consistency.
165-
ProviderFunctionResults []providers.FunctionHash
165+
ProviderFunctionResults []lang.FunctionHash
166166
}
167167

168168
// ProviderAddrs returns a list of all of the provider configuration addresses

internal/plans/planfile/tfplan.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/hashicorp/terraform/internal/addrs"
1616
"github.com/hashicorp/terraform/internal/checks"
1717
"github.com/hashicorp/terraform/internal/collections"
18+
"github.com/hashicorp/terraform/internal/lang"
1819
"github.com/hashicorp/terraform/internal/lang/globalref"
1920
"github.com/hashicorp/terraform/internal/plans"
2021
"github.com/hashicorp/terraform/internal/plans/planproto"
@@ -171,7 +172,7 @@ func readTfplan(r io.Reader) (*plans.Plan, error) {
171172

172173
for _, hash := range rawPlan.ProviderFunctionResults {
173174
plan.ProviderFunctionResults = append(plan.ProviderFunctionResults,
174-
providers.FunctionHash{
175+
lang.FunctionHash{
175176
Key: hash.Key,
176177
Result: hash.Result,
177178
},

internal/providers/functions.go

Lines changed: 3 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44
package providers
55

66
import (
7-
"crypto/sha256"
87
"fmt"
9-
"io"
10-
"log"
11-
"sync"
128

139
"github.com/zclconf/go-cty/cty"
1410
"github.com/zclconf/go-cty/cty/function"
1511

1612
"github.com/hashicorp/terraform/internal/addrs"
1713
"github.com/hashicorp/terraform/internal/configs/configschema"
14+
"github.com/hashicorp/terraform/internal/lang"
1815
)
1916

2017
type FunctionDecl struct {
@@ -59,7 +56,7 @@ type FunctionParam struct {
5956
//
6057
// The resTable argument is a shared instance of *FunctionResults, used to
6158
// check the result values from each function call.
62-
func (d FunctionDecl) BuildFunction(providerAddr addrs.Provider, name string, resTable *FunctionResults, factory func() (Interface, error)) function.Function {
59+
func (d FunctionDecl) BuildFunction(providerAddr addrs.Provider, name string, resTable *lang.FunctionResults, factory func() (Interface, error)) function.Function {
6360

6461
var params []function.Parameter
6562
var varParam *function.Parameter
@@ -123,7 +120,7 @@ func (d FunctionDecl) BuildFunction(providerAddr addrs.Provider, name string, re
123120
}
124121

125122
if resTable != nil {
126-
err = resTable.checkPrior(providerAddr, name, args, resp.Result)
123+
err = resTable.CheckPrior(providerAddr, name, args, resp.Result)
127124
if err != nil {
128125
return cty.UnknownVal(retType), err
129126
}
@@ -154,113 +151,3 @@ func (p *FunctionParam) ctyParameter() function.Parameter {
154151
AllowUnknown: p.AllowUnknownValues,
155152
}
156153
}
157-
158-
type priorResult struct {
159-
hash [sha256.Size]byte
160-
// when the result was from a current run, we keep a record of the result
161-
// value to aid in debugging. Results stored in the plan will only have the
162-
// hash to avoid bloating the plan with what could be many very large
163-
// values.
164-
value cty.Value
165-
}
166-
167-
type FunctionResults struct {
168-
mu sync.Mutex
169-
// results stores the prior result from a provider function call, keyed by
170-
// the hash of the function name and arguments.
171-
results map[[sha256.Size]byte]priorResult
172-
}
173-
174-
// NewFunctionResultsTable initializes a mapping of function calls to prior
175-
// results used to validate provider function calls. The hashes argument is an
176-
// optional slice of prior result hashes used to preload the cache.
177-
func NewFunctionResultsTable(hashes []FunctionHash) *FunctionResults {
178-
res := &FunctionResults{
179-
results: make(map[[sha256.Size]byte]priorResult),
180-
}
181-
182-
res.insertHashes(hashes)
183-
return res
184-
}
185-
186-
// checkPrior compares the function call against any cached results, and
187-
// returns an error if the result does not match a prior call.
188-
func (f *FunctionResults) checkPrior(provider addrs.Provider, name string, args []cty.Value, result cty.Value) error {
189-
argSum := sha256.New()
190-
191-
io.WriteString(argSum, provider.String())
192-
io.WriteString(argSum, "|"+name)
193-
194-
for _, arg := range args {
195-
// cty.Values have a Hash method, but it is not collision resistant. We
196-
// are going to rely on the GoString formatting instead, which gives
197-
// detailed results for all values.
198-
io.WriteString(argSum, "|"+arg.GoString())
199-
}
200-
201-
f.mu.Lock()
202-
defer f.mu.Unlock()
203-
204-
argHash := [sha256.Size]byte(argSum.Sum(nil))
205-
resHash := sha256.Sum256([]byte(result.GoString()))
206-
207-
res, ok := f.results[argHash]
208-
if !ok {
209-
f.results[argHash] = priorResult{
210-
hash: resHash,
211-
value: result,
212-
}
213-
return nil
214-
}
215-
216-
if resHash != res.hash {
217-
// Log the args for debugging in case the hcl context is
218-
// insufficient. The error should be adequate most of the time, and
219-
// could already be quite long, so we don't want to add all
220-
// arguments too.
221-
log.Printf("[ERROR] provider %s returned an inconsistent result for function %q with args: %#v\n", provider, name, args)
222-
// The hcl package will add the necessary context around the error in
223-
// the diagnostic, but we add the differing results when we can.
224-
// TODO: maybe we should add a call to action, since this is a bug in
225-
// the provider.
226-
if res.value != cty.NilVal {
227-
return fmt.Errorf("provider function returned an inconsistent result,\nwas: %#v,\nnow: %#v", res.value, result)
228-
229-
}
230-
return fmt.Errorf("provider function returned an inconsistent result")
231-
}
232-
233-
return nil
234-
}
235-
236-
// insertHashes insert key-value pairs to the functionResults map. This is used
237-
// to preload stored values before any Verify calls are made.
238-
func (f *FunctionResults) insertHashes(hashes []FunctionHash) {
239-
f.mu.Lock()
240-
defer f.mu.Unlock()
241-
242-
for _, res := range hashes {
243-
f.results[[sha256.Size]byte(res.Key)] = priorResult{
244-
hash: [sha256.Size]byte(res.Result),
245-
}
246-
}
247-
}
248-
249-
// FunctionHash contains the key and result hash values from a prior function
250-
// call.
251-
type FunctionHash struct {
252-
Key []byte
253-
Result []byte
254-
}
255-
256-
// copy the hash values into a struct which can be recorded in the plan.
257-
func (f *FunctionResults) GetHashes() []FunctionHash {
258-
f.mu.Lock()
259-
defer f.mu.Unlock()
260-
261-
var res []FunctionHash
262-
for k, r := range f.results {
263-
res = append(res, FunctionHash{Key: k[:], Result: r.hash[:]})
264-
}
265-
return res
266-
}

internal/stacks/stackplan/component.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111

1212
"github.com/hashicorp/terraform/internal/addrs"
1313
"github.com/hashicorp/terraform/internal/collections"
14+
"github.com/hashicorp/terraform/internal/lang"
1415
"github.com/hashicorp/terraform/internal/plans"
15-
"github.com/hashicorp/terraform/internal/providers"
1616
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
1717
"github.com/hashicorp/terraform/internal/states"
1818
)
@@ -69,7 +69,7 @@ type Component struct {
6969
// PlannedFunctionResults is a shared table of results from calling
7070
// provider functions. This is stored and loaded from during the planning
7171
// stage to use during apply operations.
72-
PlannedFunctionResults []providers.FunctionHash
72+
PlannedFunctionResults []lang.FunctionHash
7373

7474
// PlannedInputValues and PlannedInputValueMarks are the values that
7575
// Terraform has planned to use for input variables in this component.

0 commit comments

Comments
 (0)