Skip to content

Commit a0d2e21

Browse files
authored
helper/validation: Prevented panics with ToDiagFunc() function when used inside Schema type Elem field (#915)
Reference: hashicorp/terraform-plugin-sdk#734
1 parent 1c932cf commit a0d2e21

File tree

4 files changed

+88
-41
lines changed

4 files changed

+88
-41
lines changed

.changelog/915.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
helper/validation: Prevented panics with `ToDiagFunc()` function when used inside `Schema` type `Elem` field, such as validating `TypeList` elements
3+
```

helper/validation/meta.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,21 @@ func ToDiagFunc(validator schema.SchemaValidateFunc) schema.SchemaValidateDiagFu
6666
return func(i interface{}, p cty.Path) diag.Diagnostics {
6767
var diags diag.Diagnostics
6868

69-
attr := p[len(p)-1].(cty.GetAttrStep)
70-
ws, es := validator(i, attr.Name)
69+
// A practitioner-friendly key for any SchemaValidateFunc output.
70+
// Generally this should be the last attribute name on the path.
71+
// If not found for some unexpected reason, an empty string is fine
72+
// as the diagnostic will have the full attribute path anyways.
73+
var key string
74+
75+
// Reverse search for last cty.GetAttrStep
76+
for i := len(p) - 1; i >= 0; i-- {
77+
if pathStep, ok := p[i].(cty.GetAttrStep); ok {
78+
key = pathStep.Name
79+
break
80+
}
81+
}
82+
83+
ws, es := validator(i, key)
7184

7285
for _, w := range ws {
7386
diags = append(diags, diag.Diagnostic{

helper/validation/meta_test.go

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package validation
33
import (
44
"regexp"
55
"testing"
6+
7+
"github.com/hashicorp/go-cty/cty"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
69
)
710

811
func TestValidationNoZeroValues(t *testing.T) {
@@ -101,36 +104,94 @@ func TestValidationAny(t *testing.T) {
101104
}
102105

103106
func TestToDiagFunc(t *testing.T) {
104-
runDiagTestCases(t, []diagTestCase{
105-
{
107+
t.Parallel()
108+
109+
testCases := map[string]struct {
110+
path cty.Path
111+
val interface{}
112+
f schema.SchemaValidateDiagFunc
113+
expectedErr *regexp.Regexp
114+
}{
115+
"success-GetAttrStep-int": {
116+
path: cty.Path{
117+
cty.GetAttrStep{Name: "test_property"},
118+
},
106119
val: 43,
107120
f: ToDiagFunc(Any(
108121
IntAtLeast(42),
109122
IntAtMost(5),
110123
)),
111124
},
112-
{
125+
"success-GetAttrStep-string": {
126+
path: cty.Path{
127+
cty.GetAttrStep{Name: "test_property"},
128+
},
113129
val: "foo",
114130
f: ToDiagFunc(All(
115131
StringLenBetween(1, 10),
116132
StringIsNotWhiteSpace,
117133
)),
118134
},
119-
{
135+
"success-IndexStep-string": {
136+
path: cty.Path{
137+
cty.GetAttrStep{Name: "test_property"},
138+
cty.IndexStep{Key: cty.NumberIntVal(0)},
139+
},
140+
val: "foo",
141+
f: ToDiagFunc(StringLenBetween(1, 10)),
142+
},
143+
"error-GetAttrStep-int-first": {
144+
path: cty.Path{
145+
cty.GetAttrStep{Name: "test_property"},
146+
},
120147
val: 7,
121148
f: ToDiagFunc(Any(
122149
IntAtLeast(42),
123150
IntAtMost(5),
124151
)),
125-
expectedErr: regexp.MustCompile(`expected [\w]+ to be at least \(42\), got 7`),
152+
expectedErr: regexp.MustCompile(`expected test_property to be at least \(42\), got 7`),
126153
},
127-
{
154+
"error-GetAttrStep-int-second": {
155+
path: cty.Path{
156+
cty.GetAttrStep{Name: "test_property"},
157+
},
128158
val: 7,
129159
f: ToDiagFunc(Any(
130160
IntAtLeast(42),
131161
IntAtMost(5),
132162
)),
133-
expectedErr: regexp.MustCompile(`expected [\w]+ to be at most \(5\), got 7`),
134-
},
135-
})
163+
expectedErr: regexp.MustCompile(`expected test_property to be at most \(5\), got 7`),
164+
},
165+
"error-IndexStep-int": {
166+
path: cty.Path{
167+
cty.GetAttrStep{Name: "test_property"},
168+
cty.IndexStep{Key: cty.NumberIntVal(0)},
169+
},
170+
val: 7,
171+
f: ToDiagFunc(IntAtLeast(42)),
172+
expectedErr: regexp.MustCompile(`expected test_property to be at least \(42\), got 7`),
173+
},
174+
}
175+
176+
for name, testCase := range testCases {
177+
name, testCase := name, testCase
178+
179+
t.Run(name, func(t *testing.T) {
180+
t.Parallel()
181+
182+
diags := testCase.f(testCase.val, testCase.path)
183+
184+
if !diags.HasError() && testCase.expectedErr == nil {
185+
return
186+
}
187+
188+
if diags.HasError() && testCase.expectedErr == nil {
189+
t.Fatalf("expected to produce no errors, got %v", diags)
190+
}
191+
192+
if !matchAnyDiagSummary(diags, testCase.expectedErr) {
193+
t.Fatalf("expected to produce error matching %q, got %v", testCase.expectedErr, diags)
194+
}
195+
})
196+
}
136197
}

helper/validation/testing.go

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
testing "github.com/mitchellh/go-testing-interface"
77

8-
"github.com/hashicorp/go-cty/cty"
98
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
109
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1110
)
@@ -16,12 +15,6 @@ type testCase struct {
1615
expectedErr *regexp.Regexp
1716
}
1817

19-
type diagTestCase struct {
20-
val interface{}
21-
f schema.SchemaValidateDiagFunc
22-
expectedErr *regexp.Regexp
23-
}
24-
2518
func runTestCases(t testing.T, cases []testCase) {
2619
t.Helper()
2720

@@ -52,29 +45,6 @@ func matchAnyError(errs []error, r *regexp.Regexp) bool {
5245
return false
5346
}
5447

55-
func runDiagTestCases(t testing.T, cases []diagTestCase) {
56-
t.Helper()
57-
58-
for i, tc := range cases {
59-
p := cty.Path{
60-
cty.GetAttrStep{Name: "test_property"},
61-
}
62-
diags := tc.f(tc.val, p)
63-
64-
if !diags.HasError() && tc.expectedErr == nil {
65-
continue
66-
}
67-
68-
if diags.HasError() && tc.expectedErr == nil {
69-
t.Fatalf("expected test case %d to produce no errors, got %v", i, diags)
70-
}
71-
72-
if !matchAnyDiagSummary(diags, tc.expectedErr) {
73-
t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, diags)
74-
}
75-
}
76-
}
77-
7848
func matchAnyDiagSummary(ds diag.Diagnostics, r *regexp.Regexp) bool {
7949
for _, d := range ds {
8050
if r.MatchString(d.Summary) {

0 commit comments

Comments
 (0)