Skip to content

Commit b9138f4

Browse files
authored
terraform: validate providers' schemas during NewContext (#28124)
* checkpoint save: update InternalValidate tests to compare exact error * configschema: extract and extend attribute validation This commit adds an attribute-specific InternalValidate which was extracted directly from the block.InternalValidate logic and extended to verify any NestedTypes inside an Attribute. Only one error message changed, since it is now valid to have a cty.NilType for Attribute.Type as long as NestedType is set. * terraform: validate provider schema's during NewContext We haven't been able to guarantee that providers are validating their own schemas using (some version of) InternalValidate since providers were split out of the main codebase. This PR adds a call to InternalValidate when provider schemas are initially loaded by NewContext, which required a few other changes: InternalValidate's handling of errors vs multierrors was a little weird - before this PR, it was occasionally returning a non-nil error which only stated "0 errors occurred" - so I addressed that in InternalValidate. I then tested this with a configuration that was using all of our most popular providers, and found that at least on provider had some invalid attribute names, so I commented that particular validation out. Adding that in would be a breaking change which we would have to coordinate with enablement and providers and (especially in this case) make sure it's well communicated to external provider developers. I ran a few very unscientific tests comparing the timing with and without this validation, and it appeared to only cause a sub-second increase. * refactor validate error message to closer match the sdk's message * better error message * tweak error message: move the instruction to run init to the end of the message, after the specific error.
1 parent 77562d9 commit b9138f4

File tree

6 files changed

+229
-100
lines changed

6 files changed

+229
-100
lines changed

command/plan_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ func TestPlan_init_required(t *testing.T) {
940940
t.Fatalf("expected error, got success")
941941
}
942942
got := output.Stderr()
943-
if !strings.Contains(got, `Plugin reinitialization required. Please run "terraform init".`) {
943+
if !strings.Contains(got, `Error: Could not load plugin`) {
944944
t.Fatal("wrong error message in output:", got)
945945
}
946946
}

configs/configschema/decoder_spec.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ func (b *Block) DecoderSpec() hcldec.Spec {
9292

9393
for name, blockS := range b.BlockTypes {
9494
if _, exists := ret[name]; exists {
95-
// This indicates an invalid schema, since it's not valid to
96-
// define both an attribute and a block type of the same name.
97-
// However, we don't raise this here since it's checked by
98-
// InternalValidate.
95+
// This indicates an invalid schema, since it's not valid to define
96+
// both an attribute and a block type of the same name. We assume
97+
// that the provider has already used something like
98+
// InternalValidate to validate their schema.
9999
continue
100100
}
101101

@@ -104,7 +104,7 @@ func (b *Block) DecoderSpec() hcldec.Spec {
104104
// We can only validate 0 or 1 for MinItems, because a dynamic block
105105
// may satisfy any number of min items while only having a single
106106
// block in the config. We cannot validate MaxItems because a
107-
// configuration may have any number of dynamic blocks
107+
// configuration may have any number of dynamic blocks.
108108
minItems := 0
109109
if blockS.MinItems > 1 {
110110
minItems = 1
@@ -145,10 +145,12 @@ func (b *Block) DecoderSpec() hcldec.Spec {
145145
}
146146
case NestingSet:
147147
// We forbid dynamically-typed attributes inside NestingSet in
148-
// InternalValidate, so we don't do anything special to handle
149-
// that here. (There is no set analog to tuple and object types,
150-
// because cty's set implementation depends on knowing the static
151-
// type in order to properly compute its internal hashes.)
148+
// InternalValidate, so we don't do anything special to handle that
149+
// here. (There is no set analog to tuple and object types, because
150+
// cty's set implementation depends on knowing the static type in
151+
// order to properly compute its internal hashes.) We assume that
152+
// the provider has already used something like InternalValidate to
153+
// validate their schema.
152154
ret[name] = &hcldec.BlockSetSpec{
153155
TypeName: name,
154156
Nested: childSpec,
@@ -174,7 +176,8 @@ func (b *Block) DecoderSpec() hcldec.Spec {
174176
}
175177
default:
176178
// Invalid nesting type is just ignored. It's checked by
177-
// InternalValidate.
179+
// InternalValidate. We assume that the provider has already used
180+
// something like InternalValidate to validate their schema.
178181
continue
179182
}
180183
}
@@ -190,9 +193,10 @@ func (a *Attribute) decoderSpec(name string) hcldec.Spec {
190193
}
191194

192195
if a.NestedType != nil {
193-
// FIXME: a panic() is a bad UX. Fix this, probably by extending
194-
// InternalValidate() to check Attribute schemas as well and calling it
195-
// when we get the schema from the provider in Context().
196+
// FIXME: a panic() is a bad UX. InternalValidate() can check Attribute
197+
// schemas as well so a fix might be to call it when we get the schema
198+
// from the provider in Context(). Since this could be a breaking
199+
// change, we'd need to communicate well before adding that call.
196200
if a.Type != cty.NilType {
197201
panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
198202
}

configs/configschema/internal_validate.go

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,95 +11,161 @@ import (
1111

1212
var validName = regexp.MustCompile(`^[a-z0-9_]+$`)
1313

14-
// InternalValidate returns an error if the receiving block and its child
15-
// schema definitions have any consistencies with the documented rules for
16-
// valid schema.
14+
// InternalValidate returns an error if the receiving block and its child schema
15+
// definitions have any inconsistencies with the documented rules for valid
16+
// schema.
1717
//
18-
// This is intended to be used within unit tests to detect when a given
19-
// schema is invalid.
18+
// This can be used within unit tests to detect when a given schema is invalid,
19+
// and is run when terraform loads provider schemas during NewContext.
2020
func (b *Block) InternalValidate() error {
2121
if b == nil {
2222
return fmt.Errorf("top-level block schema is nil")
2323
}
24-
return b.internalValidate("", nil)
25-
24+
return b.internalValidate("")
2625
}
2726

28-
func (b *Block) internalValidate(prefix string, err error) error {
27+
func (b *Block) internalValidate(prefix string) error {
28+
var multiErr *multierror.Error
29+
2930
for name, attrS := range b.Attributes {
3031
if attrS == nil {
31-
err = multierror.Append(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name))
32+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: attribute schema is nil", prefix, name))
3233
continue
3334
}
34-
if !validName.MatchString(name) {
35-
err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
36-
}
37-
if !attrS.Optional && !attrS.Required && !attrS.Computed {
38-
err = multierror.Append(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name))
39-
}
40-
if attrS.Optional && attrS.Required {
41-
err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name))
42-
}
43-
if attrS.Computed && attrS.Required {
44-
err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name))
45-
}
46-
if attrS.Type == cty.NilType {
47-
err = multierror.Append(err, fmt.Errorf("%s%s: Type must be set to something other than cty.NilType", prefix, name))
48-
}
35+
multiErr = multierror.Append(multiErr, attrS.internalValidate(name, prefix))
4936
}
5037

5138
for name, blockS := range b.BlockTypes {
5239
if blockS == nil {
53-
err = multierror.Append(err, fmt.Errorf("%s%s: block schema is nil", prefix, name))
40+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: block schema is nil", prefix, name))
5441
continue
5542
}
5643

5744
if _, isAttr := b.Attributes[name]; isAttr {
58-
err = multierror.Append(err, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name))
45+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name))
5946
} else if !validName.MatchString(name) {
60-
err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
47+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
6148
}
6249

6350
if blockS.MinItems < 0 || blockS.MaxItems < 0 {
64-
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name))
51+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name))
6552
}
6653

6754
switch blockS.Nesting {
6855
case NestingSingle:
6956
switch {
7057
case blockS.MinItems != blockS.MaxItems:
71-
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name))
58+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name))
7259
case blockS.MinItems < 0 || blockS.MinItems > 1:
73-
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name))
60+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name))
7461
}
7562
case NestingGroup:
7663
if blockS.MinItems != 0 || blockS.MaxItems != 0 {
77-
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems cannot be used in NestingGroup mode", prefix, name))
64+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems cannot be used in NestingGroup mode", prefix, name))
7865
}
7966
case NestingList, NestingSet:
8067
if blockS.MinItems > blockS.MaxItems && blockS.MaxItems != 0 {
81-
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting))
68+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting))
8269
}
8370
if blockS.Nesting == NestingSet {
8471
ety := blockS.Block.ImpliedType()
8572
if ety.HasDynamicTypes() {
8673
// This is not permitted because the HCL (cty) set implementation
8774
// needs to know the exact type of set elements in order to
8875
// properly hash them, and so can't support mixed types.
89-
err = multierror.Append(err, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name))
76+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name))
9077
}
9178
}
9279
case NestingMap:
9380
if blockS.MinItems != 0 || blockS.MaxItems != 0 {
94-
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name))
81+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name))
9582
}
9683
default:
97-
err = multierror.Append(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting))
84+
multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting))
9885
}
9986

10087
subPrefix := prefix + name + "."
101-
err = blockS.Block.internalValidate(subPrefix, err)
88+
multiErr = multierror.Append(multiErr, blockS.Block.internalValidate(subPrefix))
89+
}
90+
91+
return multiErr.ErrorOrNil()
92+
}
93+
94+
// InternalValidate returns an error if the receiving attribute and its child
95+
// schema definitions have any inconsistencies with the documented rules for
96+
// valid schema.
97+
func (a *Attribute) InternalValidate(name string) error {
98+
if a == nil {
99+
return fmt.Errorf("attribute schema is nil")
100+
}
101+
return a.internalValidate(name, "")
102+
}
103+
104+
func (a *Attribute) internalValidate(name, prefix string) error {
105+
var err *multierror.Error
106+
107+
/* FIXME: this validation breaks certain existing providers and cannot be enforced without coordination.
108+
if !validName.MatchString(name) {
109+
err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
110+
}
111+
*/
112+
if !a.Optional && !a.Required && !a.Computed {
113+
err = multierror.Append(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name))
114+
}
115+
if a.Optional && a.Required {
116+
err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name))
117+
}
118+
if a.Computed && a.Required {
119+
err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name))
120+
}
121+
122+
if a.Type == cty.NilType && a.NestedType == nil {
123+
err = multierror.Append(err, fmt.Errorf("%s%s: either Type or NestedType must be defined", prefix, name))
124+
}
125+
126+
if a.Type != cty.NilType {
127+
if a.NestedType != nil {
128+
err = multierror.Append(fmt.Errorf("%s: Type and NestedType cannot both be set", name))
129+
}
130+
}
131+
132+
if a.NestedType != nil {
133+
switch a.NestedType.Nesting {
134+
case NestingSingle:
135+
switch {
136+
case a.NestedType.MinItems != a.NestedType.MaxItems:
137+
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name))
138+
case a.NestedType.MinItems < 0 || a.NestedType.MinItems > 1:
139+
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name))
140+
}
141+
case NestingList, NestingSet:
142+
if a.NestedType.MinItems > a.NestedType.MaxItems && a.NestedType.MaxItems != 0 {
143+
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, a.NestedType.Nesting))
144+
}
145+
if a.NestedType.Nesting == NestingSet {
146+
ety := a.NestedType.ImpliedType()
147+
if ety.HasDynamicTypes() {
148+
// This is not permitted because the HCL (cty) set implementation
149+
// needs to know the exact type of set elements in order to
150+
// properly hash them, and so can't support mixed types.
151+
err = multierror.Append(err, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name))
152+
}
153+
}
154+
case NestingMap:
155+
if a.NestedType.MinItems != 0 || a.NestedType.MaxItems != 0 {
156+
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name))
157+
}
158+
default:
159+
err = multierror.Append(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, a.NestedType.Nesting))
160+
}
161+
for name, attrS := range a.NestedType.Attributes {
162+
if attrS == nil {
163+
err = multierror.Append(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name))
164+
continue
165+
}
166+
err = multierror.Append(err, attrS.internalValidate(name, prefix))
167+
}
102168
}
103169

104-
return err
170+
return err.ErrorOrNil()
105171
}

0 commit comments

Comments
 (0)