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 }