/rules/terraformrules/terraform_naming_convention.go

https://github.com/wata727/tflint · Go · 232 lines · 178 code · 36 blank · 18 comment · 39 complexity · edcfca93550db9533e76e6f41311b8d2 MD5 · raw file

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