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  }