Skip to content

Commit

Permalink
New required_output rule (#16)
Browse files Browse the repository at this point in the history
* new required_output rule

* refactor: use similar framework for outputs than we do with interfaces

* test: update integration tests

---------

Co-authored-by: Matt White <[email protected]>
  • Loading branch information
lonegunmanb and matt-FFFFFF authored Apr 9, 2024
1 parent 61f6cc4 commit 6df944d
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 0 deletions.
8 changes: 8 additions & 0 deletions integration/optional-defaults-incorrect/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ variable "variable" {
resource "azurerm_lb" "test" {
sku = var.variable.default
}

output "resource" {
value = null
}

output "resource_id" {
value = null
}
8 changes: 8 additions & 0 deletions integration/optional-defaults/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ variable "variable" {
resource "azurerm_lb" "test" {
sku = var.variable.default
}

output "resource" {
value = null
}

output "resource_id" {
value = null
}
8 changes: 8 additions & 0 deletions integration/simplevaluerule-null-value/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ variable "variable" {
resource "azurerm_lb" "test" {
sku = var.variable
}

output "resource" {
value = null
}

output "resource_id" {
value = null
}
8 changes: 8 additions & 0 deletions integration/unknownrule-null-incorrect/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ variable "variable" {
resource "azurerm_virtual_machine" "test" {
zone = var.variable
}

output "resource" {
value = null
}

output "resource_id" {
value = null
}
8 changes: 8 additions & 0 deletions integration/unknownrule-null/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ variable "variable" {
resource "azurerm_virtual_machine" "test" {
zone = var.variable
}

output "resource" {
value = null
}

output "resource_id" {
value = null
}
10 changes: 10 additions & 0 deletions outputs/outputs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Package outputs provides the rules for the outputs category.
// Add the rules to the below slice to enable them.
package outputs

import "github.com/terraform-linters/tflint-plugin-sdk/tflint"

var Rules = []tflint.Rule{
NewRequiredOutputRule("required_output_tffr2", "resource", "https://azure.github.io/Azure-Verified-Modules/specs/terraform/#id-tffr2---category-outputs---additional-terraform-outputs"),
NewRequiredOutputRule("required_output_rmfr7", "resource_id", "https://azure.github.io/Azure-Verified-Modules/specs/shared/#id-rmfr7---category-outputs---minimum-required-outputs"),
}
80 changes: 80 additions & 0 deletions outputs/required_output_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package outputs_test

import (
"testing"

"github.com/Azure/tflint-ruleset-avm/outputs"
"github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/helper"
)

func TestRequiredOutput(t *testing.T) {
cases := []struct {
desc string
config string
requiredName string
issues helper.Issues
}{
{
desc: "require resource_id, ok",
config: `output "resource_id" {
value = azurerm_kubernetes_cluster.this.id
}`,
requiredName: "resource_id",
issues: helper.Issues{},
},
{
desc: "require resource_id, not ok",
config: ``,
requiredName: "resource_id",
issues: helper.Issues{
{
Rule: outputs.NewRequiredOutputRule("required_output", "resource_id", ""),
Message: "module owners MUST output the `resource_id` in their modules",
Range: hcl.Range{
Filename: "outputs.tf",
},
},
},
},
{
desc: "require resource, ok",
config: `output "resource" {
value = azurerm_kubernetes_cluster.this
}`,
requiredName: "resource",
issues: helper.Issues{},
},
{
desc: "require resource, not ok",
config: ``,
requiredName: "resource",
issues: helper.Issues{
{
Rule: outputs.NewRequiredOutputRule("required_output", "resource", ""),
Message: "module owners MUST output the `resource` in their modules",
Range: hcl.Range{
Filename: "outputs.tf",
},
},
},
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
rule := outputs.NewRequiredOutputRule("required_output", tc.requiredName, "")
filename := "variables.tf"

runner := helper.TestRunner(t, map[string]string{filename: tc.config})

if err := rule.Check(runner); err != nil {
t.Fatalf("Unexpected error occurred: %s", err)
}

helper.AssertIssuesWithoutRange(t, tc.issues, runner.Issues)
})
}
}
97 changes: 97 additions & 0 deletions outputs/requried_output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package outputs

import (
"fmt"

"github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
)

// variableBodySchema is the schema for the variable block that we want to extract from the config.
var outputBodySchema = &hclext.BodySchema{
Blocks: []hclext.BlockSchema{
{
Type: "output",
LabelNames: []string{"name"},
Body: &hclext.BodySchema{},
},
},
}

// Check interface compliance with the tflint.Rule.
var _ tflint.Rule = new(RequiredOutputRule)

// RequiredOutputRule is the struct that represents a rule that
// check for the correct usage of an interface.
type RequiredOutputRule struct {
tflint.DefaultRule
outputName string
link string
ruleName string
}

// NewRequiredOutputRule returns a new rule with the given variable.
func NewRequiredOutputRule(ruleName, requiredOutputName, link string) *RequiredOutputRule {
return &RequiredOutputRule{
ruleName: ruleName,
outputName: requiredOutputName,
link: link,
}
}

// Name returns the rule name.
func (or *RequiredOutputRule) Name() string {
return or.ruleName
}

// Link returns the link to the rule documentation.
func (or *RequiredOutputRule) Link() string {
return or.link
}

// Enabled returns whether the rule is enabled.
func (or *RequiredOutputRule) Enabled() bool {
return true
}

// Severity returns the severity of the rule.
func (or *RequiredOutputRule) Severity() tflint.Severity {
return tflint.ERROR
}

// 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 *RequiredOutputRule) 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(
outputBodySchema,
&tflint.GetModuleContentOption{ExpandMode: tflint.ExpandModeNone})
if err != nil {
return err
}

// Iterate over the outputs and check for the name we are interested in.
for _, b := range body.Blocks {
if b.Labels[0] == vcr.outputName {
return nil
}
}
return r.EmitIssue(
vcr,
fmt.Sprintf("module owners MUST output the `%s` in their modules", vcr.outputName),
hcl.Range{
Filename: "outputs.tf",
},
)
}
2 changes: 2 additions & 0 deletions rules/rule_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"slices"

"github.com/Azure/tflint-ruleset-avm/interfaces"
"github.com/Azure/tflint-ruleset-avm/outputs"

"github.com/Azure/tflint-ruleset-avm/waf"
azurerm "github.com/Azure/tflint-ruleset-azurerm-ext/rules"
Expand All @@ -27,6 +28,7 @@ var Rules = func() []tflint.Rule {
},
waf.Rules,
interfaces.Rules,
outputs.Rules,
)
}()

Expand Down

0 comments on commit 6df944d

Please sign in to comment.