github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/rules/terraformrules/terraform_naming_convention.go (about) 1 package terraformrules 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 7 "strings" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/terraform-linters/tflint/tflint" 11 ) 12 13 // TerraformNamingConventionRule checks whether blocks follow naming convention 14 type TerraformNamingConventionRule struct{} 15 16 type terraformNamingConventionRuleConfig struct { 17 Format string `hcl:"format,optional"` 18 Custom string `hcl:"custom,optional"` 19 20 CustomFormats map[string]*CustomFormatConfig `hcl:"custom_formats,optional"` 21 22 Data *BlockFormatConfig `hcl:"data,block"` 23 Locals *BlockFormatConfig `hcl:"locals,block"` 24 Module *BlockFormatConfig `hcl:"module,block"` 25 Output *BlockFormatConfig `hcl:"output,block"` 26 Resource *BlockFormatConfig `hcl:"resource,block"` 27 Variable *BlockFormatConfig `hcl:"variable,block"` 28 } 29 30 // CustomFormatConfig defines a custom format that can be used instead of the predefined formats 31 type CustomFormatConfig struct { 32 Regexp string `cty:"regex"` 33 Description string `cty:"description"` 34 } 35 36 // BlockFormatConfig defines the pre-defined format or custom regular expression to use 37 type BlockFormatConfig struct { 38 Format string `hcl:"format,optional"` 39 Custom string `hcl:"custom,optional"` 40 } 41 42 // NameValidator contains the regular expression to validate block name, if it was a named format, and the format name/regular expression string 43 type NameValidator struct { 44 Format string 45 IsNamedFormat bool 46 Regexp *regexp.Regexp 47 } 48 49 // NewTerraformNamingConventionRule returns new rule with default attributes 50 func NewTerraformNamingConventionRule() *TerraformNamingConventionRule { 51 return &TerraformNamingConventionRule{} 52 } 53 54 // Name returns the rule name 55 func (r *TerraformNamingConventionRule) Name() string { 56 return "terraform_naming_convention" 57 } 58 59 // Enabled returns whether the rule is enabled by default 60 func (r *TerraformNamingConventionRule) Enabled() bool { 61 return false 62 } 63 64 // Severity returns the rule severity 65 func (r *TerraformNamingConventionRule) Severity() string { 66 return tflint.NOTICE 67 } 68 69 // Link returns the rule reference link 70 func (r *TerraformNamingConventionRule) Link() string { 71 return tflint.ReferenceLink(r.Name()) 72 } 73 74 // Check checks whether blocks follow naming convention 75 func (r *TerraformNamingConventionRule) Check(runner *tflint.Runner) error { 76 if !runner.TFConfig.Path.IsRoot() { 77 // This rule does not evaluate child modules. 78 return nil 79 } 80 81 log.Printf("[TRACE] Check `%s` rule for `%s` runner", r.Name(), runner.TFConfigPath()) 82 83 config := terraformNamingConventionRuleConfig{} 84 config.Format = "snake_case" 85 if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { 86 return err 87 } 88 89 defaultNameValidator, err := config.getNameValidator() 90 if err != nil { 91 return fmt.Errorf("Invalid default configuration: %v", err) 92 } 93 94 var nameValidator *NameValidator 95 96 // data 97 dataBlockName := "data" 98 nameValidator, err = config.Data.getNameValidator(defaultNameValidator, &config, dataBlockName) 99 if err != nil { 100 return err 101 } 102 for _, target := range runner.TFConfig.Module.DataResources { 103 nameValidator.checkBlock(runner, r, dataBlockName, target.Name, &target.DeclRange) 104 } 105 106 // locals 107 localBlockName := "local value" 108 nameValidator, err = config.Locals.getNameValidator(defaultNameValidator, &config, localBlockName) 109 if err != nil { 110 return err 111 } 112 for _, target := range runner.TFConfig.Module.Locals { 113 nameValidator.checkBlock(runner, r, localBlockName, target.Name, &target.DeclRange) 114 } 115 116 // modules 117 moduleBlockName := "module" 118 nameValidator, err = config.Module.getNameValidator(defaultNameValidator, &config, moduleBlockName) 119 if err != nil { 120 return err 121 } 122 for _, target := range runner.TFConfig.Module.ModuleCalls { 123 nameValidator.checkBlock(runner, r, moduleBlockName, target.Name, &target.DeclRange) 124 } 125 126 // outputs 127 outputBlockName := "output" 128 nameValidator, err = config.Output.getNameValidator(defaultNameValidator, &config, outputBlockName) 129 if err != nil { 130 return err 131 } 132 for _, target := range runner.TFConfig.Module.Outputs { 133 nameValidator.checkBlock(runner, r, outputBlockName, target.Name, &target.DeclRange) 134 } 135 136 // resources 137 resourceBlockName := "resource" 138 nameValidator, err = config.Resource.getNameValidator(defaultNameValidator, &config, resourceBlockName) 139 if err != nil { 140 return err 141 } 142 for _, target := range runner.TFConfig.Module.ManagedResources { 143 nameValidator.checkBlock(runner, r, resourceBlockName, target.Name, &target.DeclRange) 144 } 145 146 // variables 147 variableBlockName := "variable" 148 nameValidator, err = config.Variable.getNameValidator(defaultNameValidator, &config, variableBlockName) 149 if err != nil { 150 return err 151 } 152 for _, target := range runner.TFConfig.Module.Variables { 153 nameValidator.checkBlock(runner, r, variableBlockName, target.Name, &target.DeclRange) 154 } 155 156 return nil 157 } 158 159 func (validator *NameValidator) checkBlock(runner *tflint.Runner, r *TerraformNamingConventionRule, blockTypeName string, blockName string, blockDeclRange *hcl.Range) { 160 if validator != nil && !validator.Regexp.MatchString(blockName) { 161 var formatType string 162 if validator.IsNamedFormat { 163 formatType = "format" 164 } else { 165 formatType = "RegExp" 166 } 167 168 runner.EmitIssue( 169 r, 170 fmt.Sprintf("%s name `%s` must match the following %s: %s", blockTypeName, blockName, formatType, validator.Format), 171 *blockDeclRange, 172 ) 173 } 174 } 175 176 func (blockFormatConfig *BlockFormatConfig) getNameValidator(defaultValidator *NameValidator, config *terraformNamingConventionRuleConfig, blockName string) (*NameValidator, error) { 177 validator := defaultValidator 178 if blockFormatConfig != nil { 179 nameValidator, err := getNameValidator(blockFormatConfig.Custom, blockFormatConfig.Format, config) 180 if err != nil { 181 return nil, fmt.Errorf("Invalid %s configuration: %v", blockName, err) 182 } 183 184 validator = nameValidator 185 } 186 return validator, nil 187 } 188 189 func (config *terraformNamingConventionRuleConfig) getNameValidator() (*NameValidator, error) { 190 return getNameValidator(config.Custom, config.Format, config) 191 } 192 193 var predefinedFormats = map[string]*regexp.Regexp{ 194 "snake_case": regexp.MustCompile("^[a-z][a-z0-9]*(_[a-z0-9]+)*$"), 195 "mixed_snake_case": regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*$"), 196 } 197 198 func getNameValidator(custom string, format string, config *terraformNamingConventionRuleConfig) (*NameValidator, error) { 199 // Prefer custom format if specified 200 if custom != "" { 201 return getCustomNameValidator(false, custom, custom) 202 } else if format != "none" { 203 customFormats := config.CustomFormats 204 customFormatConfig, exists := customFormats[format] 205 if exists { 206 return getCustomNameValidator(true, customFormatConfig.Description, customFormatConfig.Regexp) 207 } 208 209 regex, exists := predefinedFormats[strings.ToLower(format)] 210 if exists { 211 nameValidator := &NameValidator{ 212 IsNamedFormat: true, 213 Format: format, 214 Regexp: regex, 215 } 216 return nameValidator, nil 217 } 218 return nil, fmt.Errorf("`%s` is unsupported format", format) 219 } 220 221 return nil, nil 222 } 223 224 func getCustomNameValidator(isNamed bool, format, expression string) (*NameValidator, error) { 225 regex, err := regexp.Compile(expression) 226 nameValidator := &NameValidator{ 227 IsNamedFormat: isNamed, 228 Format: format, 229 Regexp: regex, 230 } 231 return nameValidator, err 232 }