Skip to content

Commit 1e9075b

Browse files
authored
Merge pull request #30226 from hashicorp/nf/dec21-derandomize-dependencies
Sort dependencies when encoding `ResourceInstanceObject`
2 parents f4eb0ed + 05d0feb commit 1e9075b

File tree

2 files changed

+59
-0
lines changed

2 files changed

+59
-0
lines changed

internal/states/instance_object.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package states
22

33
import (
4+
"sort"
5+
46
"github.com/zclconf/go-cty/cty"
57
ctyjson "github.com/zclconf/go-cty/cty/json"
68

@@ -108,6 +110,13 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
108110
return nil, err
109111
}
110112

113+
// Dependencies are collected and merged in an unordered format (using map
114+
// keys as a set), then later changed to a slice (in random ordering) to be
115+
// stored in state as an array. To avoid pointless thrashing of state in
116+
// refresh-only runs, we can either override comparison of dependency lists
117+
// (more desirable, but tricky for Reasons) or just sort when encoding.
118+
sort.Slice(o.Dependencies, func(i, j int) bool { return o.Dependencies[i].String() < o.Dependencies[j].String() })
119+
111120
return &ResourceInstanceObjectSrc{
112121
SchemaVersion: schemaVersion,
113122
AttrsJSON: src,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package states
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
"github.com/hashicorp/terraform/internal/addrs"
8+
"github.com/zclconf/go-cty/cty"
9+
)
10+
11+
func TestResourceInstanceObject_encode(t *testing.T) {
12+
value := cty.ObjectVal(map[string]cty.Value{
13+
"foo": cty.True,
14+
})
15+
// The in-memory order of resource dependencies is random, since they're an
16+
// unordered set.
17+
depsOne := []addrs.ConfigResource{
18+
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"),
19+
addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"),
20+
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"),
21+
}
22+
depsTwo := []addrs.ConfigResource{
23+
addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"),
24+
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"),
25+
addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"),
26+
}
27+
rioOne := &ResourceInstanceObject{
28+
Value: value,
29+
Status: ObjectPlanned,
30+
Dependencies: depsOne,
31+
}
32+
rioTwo := &ResourceInstanceObject{
33+
Value: value,
34+
Status: ObjectPlanned,
35+
Dependencies: depsTwo,
36+
}
37+
riosOne, err := rioOne.Encode(value.Type(), 0)
38+
if err != nil {
39+
t.Fatalf("unexpected error: %s", err)
40+
}
41+
riosTwo, err := rioTwo.Encode(value.Type(), 0)
42+
if err != nil {
43+
t.Fatalf("unexpected error: %s", err)
44+
}
45+
// However, identical sets of dependencies should always be written to state
46+
// in an identical order, so we don't do meaningless state updates on refresh.
47+
if diff := cmp.Diff(riosOne.Dependencies, riosTwo.Dependencies); diff != "" {
48+
t.Errorf("identical dependencies got encoded in different orders:\n%s", diff)
49+
}
50+
}

0 commit comments

Comments
 (0)