interfaces/interface_variable.go (207 lines of code) (raw):
package interfaces
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/matt-FFFFFF/tfvarcheck/check"
"github.com/matt-FFFFFF/tfvarcheck/varcheck"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/zclconf/go-cty/cty"
)
// variableBodySchema is the schema for the variable block that we want to extract from the config.
var variableBodySchema = &hclext.BodySchema{
Blocks: []hclext.BlockSchema{
{
Type: "variable",
LabelNames: []string{"name"},
Body: &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{Name: "type"},
{Name: "default"},
{Name: "nullable"},
},
// We do not do anything with the validation data at the moment.
Blocks: []hclext.BlockSchema{
{
Type: "validation",
Body: &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{Name: "condition"},
{Name: "error_message"},
},
},
},
},
},
},
},
}
// Check interface compliance with the tflint.Rule.
var _ tflint.Rule = new(InterfaceVarCheckRule)
// InterfaceVarCheckRule is the struct that represents a rule that
// check for the correct usage of an interface.
type InterfaceVarCheckRule struct {
tflint.DefaultRule
AvmInterface // This is the interface we are checking for.
}
// NewVarCheckRuleFromAvmInterface returns a new rule with the given variable.
func NewVarCheckRuleFromAvmInterface(ifce AvmInterface) *InterfaceVarCheckRule {
return &InterfaceVarCheckRule{
AvmInterface: ifce,
}
}
// Name returns the rule name.
func (vcr *InterfaceVarCheckRule) Name() string {
return vcr.RuleName
}
// Link returns the link to the rule documentation.
func (vcr *InterfaceVarCheckRule) Link() string {
return vcr.RuleLink
}
// Enabled returns whether the rule is enabled.
func (vcr *InterfaceVarCheckRule) Enabled() bool {
return vcr.RuleEnabled
}
// Severity returns the severity of the rule.
func (vcr *InterfaceVarCheckRule) Severity() tflint.Severity {
return vcr.RuleSeverity
}
// Check checks whether the module satisfies the interface.
// It will search for a variable with the same name as the interface.
// It will check the type, default value and nullable attributes.
func (vcr *InterfaceVarCheckRule) Check(r tflint.Runner) error {
path, err := r.GetModulePath()
if err != nil {
return err
}
if !path.IsRoot() {
// This rule does not evaluate child modules.
return nil
}
// Define the schema that we want to pull out of the module content.
body, err := r.GetModuleContent(
variableBodySchema,
&tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone})
if err != nil {
return err
}
// Iterate over the variables and check for the name we are interested in.
for _, b := range body.Blocks {
if b.Labels[0] != vcr.RuleName {
continue
}
typeAttr, c := CheckWithReturnValue(NewChecker(), getAttr(vcr, r, b, "type"))
var defaultAttr *hclext.Attribute
if vcr.Default.IsKnown() {
defaultAttr, c = CheckWithReturnValue(c, getAttr(vcr, r, b, "default"))
} else {
c = c.Check(attributeNotExist(vcr, r, b, "default"))
}
if c = c.Check(checkVarType(vcr, r, typeAttr)).
Check(checkDefaultValue(vcr, r, b, defaultAttr)).
Check(checkNullableValue(vcr, r, b)); c.err != nil {
return c.err
}
// TODO: Check validation rules.
return nil
}
return nil
}
func attributeNotExist(vcr *InterfaceVarCheckRule, r tflint.Runner, b *hclext.Block, attrName string) func() (bool, error) {
return func() (bool, error) {
_, exist := b.Body.Attributes[attrName]
if exist {
return false, r.EmitIssue(vcr, fmt.Sprintf("`%s` %s should not be declared", b.Labels[0], attrName), b.DefRange)
}
return true, nil
}
}
// getAttr returns a function that will return the attribute from a given hcl block.
// It is designed to be used with the CheckWithReturnValue function.
func getAttr(rule tflint.Rule, r tflint.Runner, b *hclext.Block, attrName string) func() (*hclext.Attribute, bool, error) {
return func() (*hclext.Attribute, bool, error) {
attr, exists := b.Body.Attributes[attrName]
if !exists {
return attr, false, r.EmitIssue(
rule,
fmt.Sprintf("`%s` %s not declared", b.Labels[0], attrName),
b.DefRange,
)
}
return attr, true, nil
}
}
// checkNullableValue checks if the nullable attribute is correct.
// It is designed to be supplied to the Checker.Check() function.
func checkNullableValue(vcr *InterfaceVarCheckRule, r tflint.Runner, b *hclext.Block) func() (bool, error) {
return func() (bool, error) {
nullableAttr, nullableExists := b.Body.Attributes["nullable"]
nullableVal := cty.NullVal(cty.Bool)
var diags hcl.Diagnostics
if nullableExists {
nullableVal, diags = nullableAttr.Expr.Value(nil)
}
if diags.HasErrors() {
return false, diags
}
// Check nullable attribute.
if ok := check.Nullable(nullableVal, vcr.Nullable); ok {
return true, nil
}
msg := "nullable should not be set."
if !vcr.Nullable {
msg = "nullable should be set to false"
}
rg := b.DefRange
if nullableAttr != nil {
rg = nullableAttr.Range
}
return false, r.EmitIssue(vcr, msg, rg)
}
}
// checkVarType checks if the type of the variable is correct.
// It is designed to be supplied to the Checker.Check() function.
func checkVarType(vcr *InterfaceVarCheckRule, r tflint.Runner, typeAttr *hclext.Attribute) func() (bool, error) {
return func() (bool, error) {
// Check if the type interface is correct.
gotType, diags := varcheck.NewTypeConstraintWithDefaultsFromExp(typeAttr.Expr)
if diags.HasErrors() {
return false, diags
}
if eq := check.EqualTypeConstraints(gotType, vcr.TypeConstraintWithDefs); !eq {
return true, r.EmitIssue(vcr,
fmt.Sprintf("variable type does not comply with the interface specification:\n\n%s", vcr.VarTypeString),
typeAttr.Range,
)
}
return true, nil
}
}
// checkDefaultValue checks if the default value of a variable is correct.
// It is designed to be supplied to the Checker.Check() function.
func checkDefaultValue(vcr *InterfaceVarCheckRule, r tflint.Runner, b *hclext.Block, defaultAttr *hclext.Attribute) func() (bool, error) {
return func() (bool, error) {
// Check if the default value is correct.
if !vcr.Default.IsKnown() {
if defaultAttr != nil {
return true, r.EmitIssue(
vcr,
fmt.Sprintf("default value should not be set, see: %s", vcr.Link()),
defaultAttr.Range,
)
}
return true, nil
}
defaultVal, _ := defaultAttr.Expr.Value(nil)
if !check.EqualCtyValue(defaultVal, vcr.Default) {
return true, r.EmitIssue(
vcr,
fmt.Sprintf("default value is not correct, see: %s", vcr.Link()),
b.DefRange,
)
}
return true, nil
}
}
// NewChecker is the constructor for the Checker type.
func NewChecker() Checker {
return Checker{
continueCheck: true,
}
}
// Checker is a struct that is used to chain checks together.
type Checker struct {
continueCheck bool
err error
}
// Check is a executes a supplied function that returns a bool and an error.
// The bool is a continueCheck value that is used to determine if the check should continue.
// The error is the error that is returned from the check.
//
// This function returns a new Checker, so it can be chained with other checks in a fluent style.
func (c Checker) Check(check func() (bool, error)) Checker {
if c.err != nil || !c.continueCheck {
return c
}
continueCheck, err := check()
return Checker{
continueCheck: continueCheck,
err: err,
}
}
// CheckWithReturnValue is a generic function that runs a check func() that, as well as
// returning a bool & error, also returns a value.
// The main function will then return the value and a new Checker with the continueCheck and err.
func CheckWithReturnValue[TR any](c Checker, check func() (TR, bool, error)) (ret TR, rc Checker) {
if c.err != nil || !c.continueCheck {
rc = c
return
}
tr, continueCheck, err := check()
return tr, Checker{
continueCheck: continueCheck,
err: err,
}
}