diff --git a/interfaces/interface_variable.go b/interfaces/interface_variable.go index cc8dd82..19ade14 100644 --- a/interfaces/interface_variable.go +++ b/interfaces/interface_variable.go @@ -106,7 +106,12 @@ func (vcr *InterfaceVarCheckRule) Check(r tflint.Runner) error { } typeAttr, c := CheckWithReturnValue(NewChecker(), getAttr(vcr, r, b, "type")) - defaultAttr, c := CheckWithReturnValue(c, getAttr(vcr, r, b, "default")) + 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 { @@ -118,7 +123,17 @@ func (vcr *InterfaceVarCheckRule) Check(r tflint.Runner) error { return nil } -// getTypeAttr returns a function that will return the type attribute from a given hcl block. +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) { @@ -187,6 +202,16 @@ func checkVarType(vcr *InterfaceVarCheckRule, r tflint.Runner, typeAttr *hclext. 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( diff --git a/interfaces/interfaces.go b/interfaces/interfaces.go index 6d29d96..f1b0013 100644 --- a/interfaces/interfaces.go +++ b/interfaces/interfaces.go @@ -8,6 +8,7 @@ import ( var Rules = []tflint.Rule{ NewVarCheckRuleFromAvmInterface(CustomerManagedKey), NewVarCheckRuleFromAvmInterface(DiagnosticSettings), + NewVarCheckRuleFromAvmInterface(Location), NewVarCheckRuleFromAvmInterface(Lock), NewVarCheckRuleFromAvmInterface(ManagedIdentities), NewVarCheckRuleFromAvmInterface(RoleAssignments), diff --git a/interfaces/interfaces_simple_test.go b/interfaces/interfaces_simple_test.go index 29ff582..e7c1951 100644 --- a/interfaces/interfaces_simple_test.go +++ b/interfaces/interfaces_simple_test.go @@ -2,12 +2,12 @@ package interfaces_test import ( "fmt" - "github.com/stretchr/testify/assert" "testing" "github.com/Azure/tflint-ruleset-avm/interfaces" "github.com/hashicorp/hcl/v2" "github.com/matt-FFFFFF/tfvarcheck/varcheck" + "github.com/stretchr/testify/assert" "github.com/terraform-linters/tflint-plugin-sdk/helper" "github.com/terraform-linters/tflint-plugin-sdk/tflint" "github.com/zclconf/go-cty/cty" @@ -30,6 +30,16 @@ var SimpleVar = interfaces.AvmInterface{ RuleSeverity: tflint.ERROR, } +// SimpleVar represents a simple variable type with no default value used for testing. +var SimpleVarNoDefault = interfaces.AvmInterface{ + VarCheck: varcheck.NewVarCheck(simpleType, cty.UnknownVal(cty.DynamicPseudoType), false), + RuleName: "simple", + VarTypeString: simpleVarTypeString, + RuleEnabled: true, + RuleLink: "https://simple", + RuleSeverity: tflint.ERROR, +} + func TestIncorrectInterfaceTypeStringShouldPanic(t *testing.T) { defer func() { err := recover() @@ -40,6 +50,76 @@ func TestIncorrectInterfaceTypeStringShouldPanic(t *testing.T) { t.Fatal("incorrect type should panic") } +func TestSimpleInterfaceNoDefault(t *testing.T) { + cases := []struct { + Name string + Content string + JSON bool + Expected helper.Issues + }{ + { + Name: "correct", + Content: toTerraformVarType(SimpleVarNoDefault), + Expected: helper.Issues{}, + }, + + { + Name: "incorrect - has default value", + Content: ` +variable "simple" { + type = object({ + kind = string + name = optional(string, null) + }) + default = {} +}`, + Expected: helper.Issues{ + { + Rule: interfaces.NewVarCheckRuleFromAvmInterface(SimpleVarNoDefault), + Message: "`simple` default should not be declared", + }, + }, + }, + { + Name: "incorrect - is nullable", + Content: ` +variable "simple" { + type = object({ + kind = string + name = optional(string, null) + }) +}`, + Expected: helper.Issues{ + { + Rule: interfaces.NewVarCheckRuleFromAvmInterface(SimpleVarNoDefault), + Message: "nullable should be set to false", + }, + }, + }, + } + + rule := interfaces.NewVarCheckRuleFromAvmInterface(SimpleVarNoDefault) + + for _, tc := range cases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + filename := "variables.tf" + if tc.JSON { + filename += ".json" + } + + runner := helper.TestRunner(t, map[string]string{filename: tc.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) + }) + } +} + func TestSimpleInterface(t *testing.T) { cases := []struct { Name string diff --git a/interfaces/interfaces_test.go b/interfaces/interfaces_test.go index bf89bcd..2aa9476 100644 --- a/interfaces/interfaces_test.go +++ b/interfaces/interfaces_test.go @@ -20,7 +20,9 @@ func toTerraformVarType(i interfaces.AvmInterface) string { SpacesBefore: 1, }, }) - varBody.SetAttributeValue("default", i.Default) + if i.Default.IsKnown() { + varBody.SetAttributeValue("default", i.Default) + } if !i.Nullable { varBody.SetAttributeValue("nullable", cty.False) } diff --git a/interfaces/location.go b/interfaces/location.go new file mode 100644 index 0000000..f728e16 --- /dev/null +++ b/interfaces/location.go @@ -0,0 +1,20 @@ +package interfaces + +import ( + "github.com/matt-FFFFFF/tfvarcheck/varcheck" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/zclconf/go-cty/cty" +) + +var LocationTypeString = `string` + +var locationType = StringToTypeConstraintWithDefaults(LocationTypeString) + +var Location = AvmInterface{ + VarCheck: varcheck.NewVarCheck(locationType, cty.UnknownVal(cty.String), false), + RuleName: "location", + VarTypeString: LocationTypeString, + RuleEnabled: true, + RuleLink: "https://azure.github.io/Azure-Verified-Modules/specs/shared/#id-rmnfr2---category-inputs---parametervariable-naming", + RuleSeverity: tflint.ERROR, +} diff --git a/interfaces/location_test.go b/interfaces/location_test.go new file mode 100644 index 0000000..993aa03 --- /dev/null +++ b/interfaces/location_test.go @@ -0,0 +1,45 @@ +package interfaces_test + +import ( + "testing" + + "github.com/Azure/tflint-ruleset-avm/interfaces" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +// TestLockTerraformVar tests Lock interface. +func TestTerraformLocationInterface(t *testing.T) { + cases := []struct { + Name string + Content string + JSON bool + Expected helper.Issues + }{ + { + Name: "correct", + Content: toTerraformVarType(interfaces.Location), + Expected: helper.Issues{}, + }, + } + + rule := interfaces.NewVarCheckRuleFromAvmInterface(interfaces.Location) + + for _, tc := range cases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + filename := "variables.tf" + if tc.JSON { + filename += ".json" + } + + runner := helper.TestRunner(t, map[string]string{filename: tc.Content}) + + if err := rule.Check(runner); err != nil { + t.Fatalf("Unexpected error occurred: %s", err) + } + + helper.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) + }) + } +}