-
Notifications
You must be signed in to change notification settings - Fork 25
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
New Checks: semantics validation on attribute reference #163
Changes from 1 commit
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 | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,6 +2,7 @@ package schema | |||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||||||||||||||||||||
"fmt" | ||||||||||||||||||||||||||||||||||||||||||||
tfschema "github.com/hashicorp/terraform-plugin-sdk/helper/schema" | ||||||||||||||||||||||||||||||||||||||||||||
"math" | ||||||||||||||||||||||||||||||||||||||||||||
"regexp" | ||||||||||||||||||||||||||||||||||||||||||||
"strconv" | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -59,3 +60,42 @@ func ParseAttributeReference(reference string) ([]string, error) { | |||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
return attributeReferenceParts, nil | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
// ValidateAttributeReference validates schema attribute reference. | ||||||||||||||||||||||||||||||||||||||||||||
// Attribute references are used in Schema fields such as AtLeastOneOf, ConflictsWith, and ExactlyOneOf. | ||||||||||||||||||||||||||||||||||||||||||||
func ValidateAttributeReference(tfresource *tfschema.Resource, reference string) ([]string, error) { | ||||||||||||||||||||||||||||||||||||||||||||
var curSchemaOrResource interface{} = tfresource | ||||||||||||||||||||||||||||||||||||||||||||
attributeReferenceParts := strings.Split(reference, ".") | ||||||||||||||||||||||||||||||||||||||||||||
for idx, attributeReferencePart := range attributeReferenceParts { | ||||||||||||||||||||||||||||||||||||||||||||
attributeReference := strings.Join(attributeReferenceParts[:idx+1], ".") | ||||||||||||||||||||||||||||||||||||||||||||
if math.Mod(float64(idx), 2) == 1 { | ||||||||||||||||||||||||||||||||||||||||||||
// For odd part, ensure it is a 0 and the containing schema has `MaxItems` set to `1`. This is because | ||||||||||||||||||||||||||||||||||||||||||||
// reference among multiple instances of the same nested block is not supported in current plugin SDK. | ||||||||||||||||||||||||||||||||||||||||||||
attributeReferencePartInt, err := strconv.Atoi(attributeReferencePart) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
configurationBlockReferenceErr := fmt.Errorf("%q configuration block attribute references must be separated by .0", attributeReference) | ||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||
return nil, configurationBlockReferenceErr | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
if attributeReferencePartInt != 0 { | ||||||||||||||||||||||||||||||||||||||||||||
return nil, configurationBlockReferenceErr | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
curSchema := curSchemaOrResource.(*tfschema.Schema) | ||||||||||||||||||||||||||||||||||||||||||||
if curSchema.MaxItems != 1 || curSchema.Type != tfschema.TypeList { | ||||||||||||||||||||||||||||||||||||||||||||
return nil, fmt.Errorf("%q configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes", attributeReference) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
curSchemaOrResource = curSchema.Elem | ||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||
// For even part, ensure it references to defined attribute | ||||||||||||||||||||||||||||||||||||||||||||
schema := curSchemaOrResource.(*tfschema.Resource).Schema[attributeReferencePart] | ||||||||||||||||||||||||||||||||||||||||||||
if schema == nil { | ||||||||||||||||||||||||||||||||||||||||||||
return nil, fmt.Errorf("%q references to unknown attribute", attributeReference) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
curSchemaOrResource = schema | ||||||||||||||||||||||||||||||||||||||||||||
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 here. We will want to be careful for unexpected types and missing key references:
Suggested change
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. A little improvement of this might be just using |
||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
return attributeReferenceParts, nil | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,86 @@ | ||||||
# S038 | ||||||
|
||||||
The S038 analyzer reports cases of Schemas which include `ConflictsWith` and have invalid schema attribute references. | ||||||
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.
Suggested change
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. Sorry for the mistake 😅 |
||||||
|
||||||
NOTE: This pass only works with Terraform resources that are fully defined in a single function. | ||||||
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. Terraform resources that meet the criteria for this check can come from a variety of source code, e.g. variables, inline declarations in
Suggested change
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. You're right |
||||||
|
||||||
## Flagged Code | ||||||
|
||||||
```go | ||||||
// Attribute reference in multi nested block is not supported | ||||||
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.
Suggested change
|
||||||
&schema.Resource{ | ||||||
Schema: map[string]*schema.Schema{ | ||||||
"x": { | ||||||
Type: schema.TypeList, | ||||||
Elem: &schema.Resource{ | ||||||
Schema: map[string]*schema.Schema{ | ||||||
"foo": { | ||||||
AtLeastOneOf: []string{"x.0.bar"}, | ||||||
}, | ||||||
"bar": { | ||||||
AtLeastOneOf: []string{"x.0.foo"}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
} | ||||||
``` | ||||||
|
||||||
or | ||||||
|
||||||
```go | ||||||
// Non-existed attribute reference | ||||||
&schema.Resource{ | ||||||
Schema: map[string]*schema.Schema{ | ||||||
"x": { | ||||||
Type: schema.TypeList, | ||||||
MaxItems: 1, | ||||||
Elem: &schema.Resource{ | ||||||
Schema: map[string]*schema.Schema{ | ||||||
"foo": { | ||||||
AtLeastOneOf: []string{"x.1.bar"}, | ||||||
}, | ||||||
"bar": { | ||||||
AtLeastOneOf: []string{"x.1.foo"}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
} | ||||||
``` | ||||||
|
||||||
## Passing Code | ||||||
|
||||||
```go | ||||||
&schema.Resource{ | ||||||
Schema: map[string]*schema.Schema{ | ||||||
"x": { | ||||||
Type: schema.TypeList, | ||||||
MaxItems: 1, | ||||||
Elem: &schema.Resource{ | ||||||
Schema: map[string]*schema.Schema{ | ||||||
"foo": { | ||||||
AtLeastOneOf: []string{"x.0.bar"}, | ||||||
}, | ||||||
"bar": { | ||||||
AtLeastOneOf: []string{"x.0.foo"}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
}, | ||||||
} | ||||||
``` | ||||||
|
||||||
## Ignoring Reports | ||||||
|
||||||
Singular reports can be ignored by adding the a `//lintignore:S038` Go code comment at the end of the offending line or on the line immediately proceding, e.g. | ||||||
|
||||||
```go | ||||||
//lintignore:S038 | ||||||
&schema.Resource{ | ||||||
// ... | ||||||
} | ||||||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package S038 | ||
|
||
import ( | ||
"github.com/bflad/tfproviderlint/helper/analysisutils" | ||
"github.com/bflad/tfproviderlint/helper/terraformtype/helper/schema" | ||
) | ||
|
||
var Analyzer = analysisutils.SchemaAttributeReferencesSemanticsAnalyzer("S038", schema.SchemaFieldAtLeastOneOf) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package S038 | ||
|
||
import ( | ||
"testing" | ||
|
||
"golang.org/x/tools/go/analysis/analysistest" | ||
) | ||
|
||
func TestS038(t *testing.T) { | ||
testdata := analysistest.TestData() | ||
analysistest.Run(t, testdata, Analyzer, "a") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package a | ||
|
||
import ( | ||
s "github.com/hashicorp/terraform-plugin-sdk/helper/schema" | ||
) | ||
|
||
func falias() { | ||
_ = &s.Resource{ | ||
Read: func(*s.ResourceData, interface{}) error { return nil }, | ||
Schema: map[string]*s.Schema{ | ||
"x": { | ||
Type: s.TypeList, | ||
Elem: &s.Resource{ | ||
Schema: map[string]*s.Schema{ | ||
"foo": { | ||
AtLeastOneOf: []string{"x.0.bar"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` | ||
}, | ||
"bar": { | ||
AtLeastOneOf: []string{"x.0.foo"}, // want `S038: invalid AtLeastOneOf attribute reference semantics: "x.0" configuration block attribute references are only valid for TypeList and MaxItems: 1 attributes` | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
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.
Please introduce type assertion safety here with a
switch
statement utilizing.(type)
: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.
Sure. Also for the default case (which means we are not capable to deal with for now), I think we shall just return
nil
to avoid false positive.