github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/rules/terraformrules/terraform_standard_module_structure.go (about)

     1  package terraformrules
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"path/filepath"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/terraform-linters/tflint/tflint"
    10  )
    11  
    12  const (
    13  	filenameMain      = "main.tf"
    14  	filenameVariables = "variables.tf"
    15  	filenameOutputs   = "outputs.tf"
    16  )
    17  
    18  // TerraformStandardModuleStructureRule checks whether modules adhere to Terraform's standard module structure
    19  type TerraformStandardModuleStructureRule struct{}
    20  
    21  // NewTerraformStandardModuleStructureRule returns a new rule
    22  func NewTerraformStandardModuleStructureRule() *TerraformStandardModuleStructureRule {
    23  	return &TerraformStandardModuleStructureRule{}
    24  }
    25  
    26  // Name returns the rule name
    27  func (r *TerraformStandardModuleStructureRule) Name() string {
    28  	return "terraform_standard_module_structure"
    29  }
    30  
    31  // Enabled returns whether the rule is enabled by default
    32  func (r *TerraformStandardModuleStructureRule) Enabled() bool {
    33  	return false
    34  }
    35  
    36  // Severity returns the rule severity
    37  func (r *TerraformStandardModuleStructureRule) Severity() string {
    38  	return tflint.WARNING
    39  }
    40  
    41  // Link returns the rule reference link
    42  func (r *TerraformStandardModuleStructureRule) Link() string {
    43  	return tflint.ReferenceLink(r.Name())
    44  }
    45  
    46  // Check emits errors for any missing files and any block types that are included in the wrong file
    47  func (r *TerraformStandardModuleStructureRule) Check(runner *tflint.Runner) error {
    48  	if !runner.TFConfig.Path.IsRoot() {
    49  		// This rule does not evaluate child modules.
    50  		return nil
    51  	}
    52  
    53  	log.Printf("[TRACE] Check `%s` rule for `%s` runner", r.Name(), runner.TFConfigPath())
    54  
    55  	r.checkFiles(runner)
    56  	r.checkVariables(runner)
    57  	r.checkOutputs(runner)
    58  
    59  	return nil
    60  }
    61  
    62  func (r *TerraformStandardModuleStructureRule) checkFiles(runner *tflint.Runner) {
    63  	if r.onlyJSON(runner) {
    64  		return
    65  	}
    66  
    67  	files := runner.Files()
    68  	for name, file := range files {
    69  		files[filepath.Base(name)] = file
    70  	}
    71  
    72  	if files[filenameMain] == nil {
    73  		runner.EmitIssue(
    74  			r,
    75  			fmt.Sprintf("Module should include a %s file as the primary entrypoint", filenameMain),
    76  			hcl.Range{
    77  				Filename: filepath.Join(runner.TFConfig.Module.SourceDir, filenameMain),
    78  				Start:    hcl.InitialPos,
    79  			},
    80  		)
    81  	}
    82  
    83  	if files[filenameVariables] == nil && len(runner.TFConfig.Module.Variables) == 0 {
    84  		runner.EmitIssue(
    85  			r,
    86  			fmt.Sprintf("Module should include an empty %s file", filenameVariables),
    87  			hcl.Range{
    88  				Filename: filepath.Join(runner.TFConfig.Module.SourceDir, filenameVariables),
    89  				Start:    hcl.InitialPos,
    90  			},
    91  		)
    92  	}
    93  
    94  	if files[filenameOutputs] == nil && len(runner.TFConfig.Module.Outputs) == 0 {
    95  		runner.EmitIssue(
    96  			r,
    97  			fmt.Sprintf("Module should include an empty %s file", filenameOutputs),
    98  			hcl.Range{
    99  				Filename: filepath.Join(runner.TFConfig.Module.SourceDir, filenameOutputs),
   100  				Start:    hcl.InitialPos,
   101  			},
   102  		)
   103  	}
   104  }
   105  
   106  func (r *TerraformStandardModuleStructureRule) checkVariables(runner *tflint.Runner) {
   107  	for _, variable := range runner.TFConfig.Module.Variables {
   108  		if filename := variable.DeclRange.Filename; r.shouldMove(filename, filenameVariables) {
   109  			runner.EmitIssue(
   110  				r,
   111  				fmt.Sprintf("variable %q should be moved from %s to %s", variable.Name, filename, filenameVariables),
   112  				variable.DeclRange,
   113  			)
   114  		}
   115  	}
   116  }
   117  
   118  func (r *TerraformStandardModuleStructureRule) checkOutputs(runner *tflint.Runner) {
   119  	for _, variable := range runner.TFConfig.Module.Outputs {
   120  		if filename := variable.DeclRange.Filename; r.shouldMove(filename, filenameOutputs) {
   121  			runner.EmitIssue(
   122  				r,
   123  				fmt.Sprintf("output %q should be moved from %s to %s", variable.Name, filename, filenameOutputs),
   124  				variable.DeclRange,
   125  			)
   126  		}
   127  	}
   128  }
   129  
   130  func (r *TerraformStandardModuleStructureRule) onlyJSON(runner *tflint.Runner) bool {
   131  	files := runner.Files()
   132  
   133  	if len(files) == 0 {
   134  		return false
   135  	}
   136  
   137  	for filename := range files {
   138  		if filepath.Ext(filename) != ".json" {
   139  			return false
   140  		}
   141  	}
   142  
   143  	return true
   144  }
   145  
   146  func (r *TerraformStandardModuleStructureRule) shouldMove(path string, expected string) bool {
   147  	// json files are likely generated and conventional filenames do not apply
   148  	if filepath.Ext(path) == ".json" {
   149  		return false
   150  	}
   151  
   152  	return filepath.Base(path) != expected
   153  }