-
Notifications
You must be signed in to change notification settings - Fork 234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: AtLeastOneOf
and ExactlyOneOf
validation flags added to schema
#225
Changes from all commits
e74c627
93e6a88
01fa1ea
dcb54b3
efe7f1c
17cc721
9a1d70d
d832ec8
2dd70af
990d7a5
b2f6aa3
8c7dd1a
9780cee
e81adb3
cf5435b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -216,7 +216,16 @@ type Schema struct { | |
// This will only check that they're set in the _config_. This will not | ||
// raise an error for a malfunctioning resource that sets a conflicting | ||
// key. | ||
// | ||
// ExactlyOneOf is a set of schema keys that, when set, only one of the | ||
// keys in that list can be specified. It will error if none are | ||
// specified as well. | ||
// | ||
// AtLeastOneOf is a set of schema keys that, when set, at least one of | ||
// the keys in that list must be specified. | ||
ConflictsWith []string | ||
ExactlyOneOf []string | ||
AtLeastOneOf []string | ||
|
||
// When Deprecated is set, this attribute is deprecated. | ||
// | ||
|
@@ -749,36 +758,32 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro | |
return fmt.Errorf("%s: ConflictsWith cannot be set with Required", k) | ||
} | ||
|
||
if len(v.ConflictsWith) > 0 { | ||
for _, key := range v.ConflictsWith { | ||
parts := strings.Split(key, ".") | ||
sm := topSchemaMap | ||
var target *Schema | ||
for _, part := range parts { | ||
// Skip index fields | ||
if _, err := strconv.Atoi(part); err == nil { | ||
continue | ||
} | ||
if len(v.ExactlyOneOf) > 0 && v.Required { | ||
return fmt.Errorf("%s: ExactlyOneOf cannot be set with Required", k) | ||
} | ||
|
||
var ok bool | ||
if target, ok = sm[part]; !ok { | ||
return fmt.Errorf("%s: ConflictsWith references unknown attribute (%s) at part (%s)", k, key, part) | ||
} | ||
if len(v.AtLeastOneOf) > 0 && v.Required { | ||
return fmt.Errorf("%s: AtLeastOneOf cannot be set with Required", k) | ||
} | ||
|
||
if subResource, ok := target.Elem.(*Resource); ok { | ||
sm = schemaMap(subResource.Schema) | ||
} | ||
} | ||
if target == nil { | ||
return fmt.Errorf("%s: ConflictsWith cannot find target attribute (%s), sm: %#v", k, key, sm) | ||
} | ||
if target.Required { | ||
return fmt.Errorf("%s: ConflictsWith cannot contain Required attribute (%s)", k, key) | ||
} | ||
if len(v.ConflictsWith) > 0 { | ||
err := checkKeysAgainstSchemaFlags(k, v.ConflictsWith, topSchemaMap) | ||
if err != nil { | ||
return fmt.Errorf("ConflictsWith: %+v", err) | ||
} | ||
} | ||
|
||
if len(target.ComputedWhen) > 0 { | ||
return fmt.Errorf("%s: ConflictsWith cannot contain Computed(When) attribute (%s)", k, key) | ||
} | ||
if len(v.ExactlyOneOf) > 0 { | ||
err := checkKeysAgainstSchemaFlags(k, v.ExactlyOneOf, topSchemaMap) | ||
if err != nil { | ||
return fmt.Errorf("ExactlyOneOf: %+v", err) | ||
} | ||
} | ||
|
||
if len(v.AtLeastOneOf) > 0 { | ||
err := checkKeysAgainstSchemaFlags(k, v.AtLeastOneOf, topSchemaMap) | ||
if err != nil { | ||
return fmt.Errorf("AtLeastOneOf: %+v", err) | ||
} | ||
} | ||
|
||
|
@@ -845,6 +850,40 @@ func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) erro | |
return nil | ||
} | ||
|
||
func checkKeysAgainstSchemaFlags(k string, keys []string, topSchemaMap schemaMap) error { | ||
for _, key := range keys { | ||
parts := strings.Split(key, ".") | ||
sm := topSchemaMap | ||
var target *Schema | ||
for _, part := range parts { | ||
// Skip index fields | ||
if _, err := strconv.Atoi(part); err == nil { | ||
continue | ||
} | ||
|
||
var ok bool | ||
if target, ok = sm[part]; !ok { | ||
return fmt.Errorf("%s references unknown attribute (%s) at part (%s)", k, key, part) | ||
} | ||
|
||
if subResource, ok := target.Elem.(*Resource); ok { | ||
sm = schemaMap(subResource.Schema) | ||
} | ||
} | ||
if target == nil { | ||
return fmt.Errorf("%s cannot find target attribute (%s), sm: %#v", k, key, sm) | ||
} | ||
if target.Required { | ||
return fmt.Errorf("%s cannot contain Required attribute (%s)", k, key) | ||
} | ||
|
||
if len(target.ComputedWhen) > 0 { | ||
return fmt.Errorf("%s cannot contain Computed(When) attribute (%s)", k, key) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func isValidFieldName(name string) bool { | ||
re := regexp.MustCompile("^[a-z0-9_]+$") | ||
return re.MatchString(name) | ||
|
@@ -1350,12 +1389,22 @@ func (m schemaMap) validate( | |
// We're okay as long as we had a value set | ||
ok = raw != nil | ||
} | ||
|
||
err := validateExactlyOneAttribute(k, schema, c) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
err = validateAtLeastOneAttribute(k, schema, c) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
||
if !ok { | ||
if schema.Required { | ||
return nil, []error{fmt.Errorf( | ||
"%q: required field is not set", k)} | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
|
@@ -1377,7 +1426,7 @@ func (m schemaMap) validate( | |
return nil, nil | ||
} | ||
|
||
err := m.validateConflictingAttributes(k, schema, c) | ||
err = validateConflictingAttributes(k, schema, c) | ||
if err != nil { | ||
return nil, []error{err} | ||
} | ||
|
@@ -1407,7 +1456,7 @@ func isWhollyKnown(raw interface{}) bool { | |
} | ||
return true | ||
} | ||
func (m schemaMap) validateConflictingAttributes( | ||
func validateConflictingAttributes( | ||
k string, | ||
schema *Schema, | ||
c *terraform.ResourceConfig) error { | ||
|
@@ -1431,6 +1480,79 @@ func (m schemaMap) validateConflictingAttributes( | |
return nil | ||
} | ||
|
||
func removeDuplicates(elements []string) []string { | ||
encountered := make(map[string]struct{}, 0) | ||
result := []string{} | ||
|
||
for v := range elements { | ||
if _, ok := encountered[elements[v]]; !ok { | ||
encountered[elements[v]] = struct{}{} | ||
result = append(result, elements[v]) | ||
} | ||
} | ||
|
||
return result | ||
} | ||
|
||
func validateExactlyOneAttribute( | ||
k string, | ||
schema *Schema, | ||
c *terraform.ResourceConfig) error { | ||
|
||
if len(schema.ExactlyOneOf) == 0 { | ||
return nil | ||
} | ||
|
||
allKeys := removeDuplicates(append(schema.ExactlyOneOf, k)) | ||
sort.Strings(allKeys) | ||
specified := make([]string, 0) | ||
unknownVariableValueCount := 0 | ||
for _, exactlyOneOfKey := range allKeys { | ||
if raw, ok := c.Get(exactlyOneOfKey); ok { | ||
if raw == hcl2shim.UnknownVariableValue { | ||
// This aims to do a best effort check that at least one value is specified whether | ||
// it's known or not. | ||
unknownVariableValueCount++ | ||
continue | ||
} | ||
specified = append(specified, exactlyOneOfKey) | ||
} | ||
} | ||
|
||
if len(specified) == 0 && unknownVariableValueCount == 0 { | ||
return fmt.Errorf("%q: one of `%s` must be specified", k, strings.Join(allKeys, ",")) | ||
} | ||
|
||
if len(specified) > 1 { | ||
return fmt.Errorf("%q: only one of `%s` can be specified, but `%s` were specified.", k, strings.Join(allKeys, ","), strings.Join(specified, ",")) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func validateAtLeastOneAttribute( | ||
k string, | ||
schema *Schema, | ||
c *terraform.ResourceConfig) error { | ||
|
||
if len(schema.AtLeastOneOf) == 0 { | ||
return nil | ||
} | ||
|
||
allKeys := removeDuplicates(append(schema.AtLeastOneOf, k)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my brain is tingling about this LOC in both functions, but if the tests pass I'm sure it all makes sense. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was added to ensure that users coming from the
We wanted users to go a different route where you specify the whole list:
but we don't have anyway to enforce that they use this pattern so we add the current attribute key and then remove duplicates to ensure our validation doesn't go sideways |
||
sort.Strings(allKeys) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above do we need to sort? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing it makes for nicer printout in the error message having the keys being sorted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly! |
||
|
||
for _, atLeastOneOfKey := range allKeys { | ||
if _, ok := c.Get(atLeastOneOfKey); ok { | ||
// We can ignore hcl2shim.UnknownVariable by assuming it's been set and additional validation elsewhere | ||
// will uncover this if it is in fact null. | ||
return nil | ||
} | ||
} | ||
|
||
return fmt.Errorf("%q: one of `%s` must be specified", k, strings.Join(allKeys, ",")) | ||
} | ||
|
||
func (m schemaMap) validateList( | ||
k string, | ||
raw interface{}, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to sort?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for nicer error messages. These have the potential to stack up on one another so if we sort them, they will line up nicely with one another