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

     1  package tflint
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  
     9  	hcl "github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/gohcl"
    11  	"github.com/hashicorp/hcl/v2/hclparse"
    12  	homedir "github.com/mitchellh/go-homedir"
    13  	tfplugin "github.com/terraform-linters/tflint-plugin-sdk/tflint"
    14  	"github.com/terraform-linters/tflint/client"
    15  )
    16  
    17  var defaultConfigFile = ".tflint.hcl"
    18  var fallbackConfigFile = "~/.tflint.hcl"
    19  
    20  var removedRulesMap = map[string]string{
    21  	"terraform_dash_in_data_source_name": "`terraform_dash_in_data_source_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead",
    22  	"terraform_dash_in_module_name":      "`terraform_dash_in_module_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead",
    23  	"terraform_dash_in_output_name":      "`terraform_dash_in_output_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead",
    24  	"terraform_dash_in_resource_name":    "`terraform_dash_in_resource_name` rule was removed in v0.16.0. Please use `terraform_naming_convention` rule instead",
    25  }
    26  
    27  type rawConfig struct {
    28  	Config *struct {
    29  		Module            *bool              `hcl:"module"`
    30  		DeepCheck         *bool              `hcl:"deep_check"`
    31  		Force             *bool              `hcl:"force"`
    32  		AwsCredentials    *map[string]string `hcl:"aws_credentials"`
    33  		IgnoreModule      *map[string]bool   `hcl:"ignore_module"`
    34  		Varfile           *[]string          `hcl:"varfile"`
    35  		Variables         *[]string          `hcl:"variables"`
    36  		DisabledByDefault *bool              `hcl:"disabled_by_default"`
    37  		// Removed options
    38  		TerraformVersion *string          `hcl:"terraform_version"`
    39  		IgnoreRule       *map[string]bool `hcl:"ignore_rule"`
    40  	} `hcl:"config,block"`
    41  	Rules   []RuleConfig   `hcl:"rule,block"`
    42  	Plugins []PluginConfig `hcl:"plugin,block"`
    43  }
    44  
    45  // Config describes the behavior of TFLint
    46  type Config struct {
    47  	Module            bool
    48  	DeepCheck         bool
    49  	Force             bool
    50  	AwsCredentials    client.AwsCredentials
    51  	IgnoreModules     map[string]bool
    52  	Varfiles          []string
    53  	Variables         []string
    54  	DisabledByDefault bool
    55  	Rules             map[string]*RuleConfig
    56  	Plugins           map[string]*PluginConfig
    57  }
    58  
    59  // RuleConfig is a TFLint's rule config
    60  type RuleConfig struct {
    61  	Name    string   `hcl:"name,label"`
    62  	Enabled bool     `hcl:"enabled"`
    63  	Body    hcl.Body `hcl:",remain"`
    64  }
    65  
    66  // PluginConfig is a TFLint's plugin config
    67  type PluginConfig struct {
    68  	Name    string `hcl:"name,label"`
    69  	Enabled bool   `hcl:"enabled"`
    70  }
    71  
    72  // EmptyConfig returns default config
    73  // It is mainly used for testing
    74  func EmptyConfig() *Config {
    75  	return &Config{
    76  		Module:            false,
    77  		DeepCheck:         false,
    78  		Force:             false,
    79  		AwsCredentials:    client.AwsCredentials{},
    80  		IgnoreModules:     map[string]bool{},
    81  		Varfiles:          []string{},
    82  		Variables:         []string{},
    83  		DisabledByDefault: false,
    84  		Rules:             map[string]*RuleConfig{},
    85  		Plugins:           map[string]*PluginConfig{},
    86  	}
    87  }
    88  
    89  // LoadConfig loads TFLint config from file
    90  // If failed to load the default config file, it tries to load config file under the home directory
    91  // Therefore, if there is no default config file, it will not return an error
    92  func LoadConfig(file string) (*Config, error) {
    93  	log.Printf("[INFO] Load config: %s", file)
    94  	if _, err := os.Stat(file); !os.IsNotExist(err) {
    95  		cfg, err := loadConfigFromFile(file)
    96  		if err != nil {
    97  			log.Printf("[ERROR] %s", err)
    98  			return nil, err
    99  		}
   100  		return cfg, nil
   101  	} else if file != defaultConfigFile {
   102  		log.Printf("[ERROR] %s", err)
   103  		return nil, fmt.Errorf("`%s` is not found", file)
   104  	} else {
   105  		log.Printf("[INFO] Default config file is not found. Ignored")
   106  	}
   107  
   108  	fallback, err := homedir.Expand(fallbackConfigFile)
   109  	if err != nil {
   110  		log.Printf("[ERROR] %s", err)
   111  		return nil, err
   112  	}
   113  
   114  	log.Printf("[INFO] Load fallback config: %s", fallback)
   115  	if _, err := os.Stat(fallback); !os.IsNotExist(err) {
   116  		cfg, err := loadConfigFromFile(fallback)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  		return cfg, nil
   121  	}
   122  	log.Printf("[INFO] Fallback config file is not found. Ignored")
   123  
   124  	log.Print("[INFO] Use default config")
   125  	return EmptyConfig(), nil
   126  }
   127  
   128  // Merge returns a merged copy of the two configs
   129  // Since the argument takes precedence, it can be used as overwriting of the config
   130  func (c *Config) Merge(other *Config) *Config {
   131  	ret := c.copy()
   132  
   133  	if other.Module {
   134  		ret.Module = true
   135  	}
   136  	if other.DeepCheck {
   137  		ret.DeepCheck = true
   138  	}
   139  	if other.Force {
   140  		ret.Force = true
   141  	}
   142  	if other.DisabledByDefault {
   143  		ret.DisabledByDefault = true
   144  	}
   145  
   146  	ret.AwsCredentials = ret.AwsCredentials.Merge(other.AwsCredentials)
   147  	ret.IgnoreModules = mergeBoolMap(ret.IgnoreModules, other.IgnoreModules)
   148  	ret.Varfiles = append(ret.Varfiles, other.Varfiles...)
   149  	ret.Variables = append(ret.Variables, other.Variables...)
   150  
   151  	ret.Rules = mergeRuleMap(ret.Rules, other.Rules, other.DisabledByDefault)
   152  	ret.Plugins = mergePluginMap(ret.Plugins, other.Plugins)
   153  
   154  	return ret
   155  }
   156  
   157  // ToPluginConfig converts self into the plugin configuration format
   158  func (c *Config) ToPluginConfig() *tfplugin.Config {
   159  	cfg := &tfplugin.Config{
   160  		Rules:             map[string]*tfplugin.RuleConfig{},
   161  		DisabledByDefault: c.DisabledByDefault,
   162  	}
   163  	for _, rule := range c.Rules {
   164  		cfg.Rules[rule.Name] = &tfplugin.RuleConfig{
   165  			Name:    rule.Name,
   166  			Enabled: rule.Enabled,
   167  		}
   168  	}
   169  	return cfg
   170  }
   171  
   172  // RuleSet is an interface to handle plugin's RuleSet and core RuleSet both
   173  // In the future, when all RuleSets are cut out into plugins, it will no longer be needed.
   174  type RuleSet interface {
   175  	RuleSetName() (string, error)
   176  	RuleSetVersion() (string, error)
   177  	RuleNames() ([]string, error)
   178  }
   179  
   180  // ValidateRules checks for duplicate rule names, for invalid rule names, and so on.
   181  func (c *Config) ValidateRules(rulesets ...RuleSet) error {
   182  	rulesMap := map[string]string{}
   183  	for _, ruleset := range rulesets {
   184  		ruleNames, err := ruleset.RuleNames()
   185  		if err != nil {
   186  			return err
   187  		}
   188  
   189  		for _, rule := range ruleNames {
   190  			rulesetName, err := ruleset.RuleSetName()
   191  			if err != nil {
   192  				return err
   193  			}
   194  
   195  			if existsName, exists := rulesMap[rule]; exists {
   196  				return fmt.Errorf("`%s` is duplicated in %s and %s", rule, existsName, rulesetName)
   197  			}
   198  			rulesMap[rule] = rulesetName
   199  		}
   200  	}
   201  
   202  	for _, rule := range c.Rules {
   203  		if _, exists := rulesMap[rule.Name]; !exists {
   204  			if message, exists := removedRulesMap[rule.Name]; exists {
   205  				return errors.New(message)
   206  			}
   207  			return fmt.Errorf("Rule not found: %s", rule.Name)
   208  		}
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func (c *Config) copy() *Config {
   215  	ignoreModules := make(map[string]bool)
   216  	for k, v := range c.IgnoreModules {
   217  		ignoreModules[k] = v
   218  	}
   219  
   220  	varfiles := make([]string, len(c.Varfiles))
   221  	copy(varfiles, c.Varfiles)
   222  
   223  	variables := make([]string, len(c.Variables))
   224  	copy(variables, c.Variables)
   225  
   226  	rules := map[string]*RuleConfig{}
   227  	for k, v := range c.Rules {
   228  		rules[k] = &RuleConfig{}
   229  		*rules[k] = *v
   230  	}
   231  
   232  	plugins := map[string]*PluginConfig{}
   233  	for k, v := range c.Plugins {
   234  		plugins[k] = &PluginConfig{}
   235  		*plugins[k] = *v
   236  	}
   237  
   238  	return &Config{
   239  		Module:            c.Module,
   240  		DeepCheck:         c.DeepCheck,
   241  		Force:             c.Force,
   242  		AwsCredentials:    c.AwsCredentials,
   243  		IgnoreModules:     ignoreModules,
   244  		Varfiles:          varfiles,
   245  		Variables:         variables,
   246  		DisabledByDefault: c.DisabledByDefault,
   247  		Rules:             rules,
   248  		Plugins:           plugins,
   249  	}
   250  }
   251  
   252  func loadConfigFromFile(file string) (*Config, error) {
   253  	parser := hclparse.NewParser()
   254  
   255  	f, diags := parser.ParseHCLFile(file)
   256  	if diags.HasErrors() {
   257  		return nil, diags
   258  	}
   259  
   260  	var raw rawConfig
   261  	diags = gohcl.DecodeBody(f.Body, nil, &raw)
   262  	if diags.HasErrors() {
   263  		return nil, diags
   264  	}
   265  
   266  	if raw.Config != nil {
   267  		if raw.Config.TerraformVersion != nil {
   268  			return nil, errors.New("`terraform_version` was removed in v0.9.0 because the option is no longer used")
   269  		}
   270  
   271  		if raw.Config.IgnoreRule != nil {
   272  			return nil, errors.New("`ignore_rule` was removed in v0.12.0. Please define `rule` block with `enabled = false` instead")
   273  		}
   274  	}
   275  
   276  	cfg := raw.toConfig()
   277  	log.Printf("[DEBUG] Config loaded")
   278  	log.Printf("[DEBUG]   Module: %t", cfg.Module)
   279  	log.Printf("[DEBUG]   DeepCheck: %t", cfg.DeepCheck)
   280  	log.Printf("[DEBUG]   Force: %t", cfg.Force)
   281  	log.Printf("[DEBUG]   IgnoreModules: %#v", cfg.IgnoreModules)
   282  	log.Printf("[DEBUG]   Varfiles: %#v", cfg.Varfiles)
   283  	log.Printf("[DEBUG]   Variables: %#v", cfg.Variables)
   284  	log.Printf("[DEBUG]   DisabledByDefault: %#v", cfg.DisabledByDefault)
   285  	log.Printf("[DEBUG]   Rules: %#v", cfg.Rules)
   286  	log.Printf("[DEBUG]   Plugins: %#v", cfg.Plugins)
   287  
   288  	return cfg, nil
   289  }
   290  
   291  func mergeBoolMap(a, b map[string]bool) map[string]bool {
   292  	ret := map[string]bool{}
   293  	for k, v := range a {
   294  		ret[k] = v
   295  	}
   296  	for k, v := range b {
   297  		ret[k] = v
   298  	}
   299  	return ret
   300  }
   301  
   302  func mergeRuleMap(a, b map[string]*RuleConfig, bDisabledByDefault bool) map[string]*RuleConfig {
   303  	ret := map[string]*RuleConfig{}
   304  	if bDisabledByDefault {
   305  		for bK, bV := range b {
   306  			configRuleFound := false
   307  			for aK, aV := range a {
   308  				if aK == bK {
   309  					ret[bK] = bV
   310  					ret[bK].Body = aV.Body
   311  					ret[bK].Enabled = true
   312  					configRuleFound = true
   313  				}
   314  			}
   315  			if !configRuleFound {
   316  				ret[bK] = bV
   317  				ret[bK].Enabled = true
   318  			}
   319  		}
   320  		return ret
   321  	}
   322  
   323  	for k, v := range a {
   324  		ret[k] = v
   325  	}
   326  	for k, v := range b {
   327  		// HACK: If you enable the rule through the CLI instead of the file, its hcl.Body will not contain valid range.
   328  		// @see https://github.com/hashicorp/hcl/blob/v2.5.0/merged.go#L132-L135
   329  		if prevConfig, exists := ret[k]; exists && v.Body.MissingItemRange().Filename == "<empty>" {
   330  			ret[k] = v
   331  			// Do not override body
   332  			ret[k].Body = prevConfig.Body
   333  		} else {
   334  			ret[k] = v
   335  		}
   336  	}
   337  	return ret
   338  }
   339  
   340  func mergePluginMap(a, b map[string]*PluginConfig) map[string]*PluginConfig {
   341  	ret := map[string]*PluginConfig{}
   342  	for k, v := range a {
   343  		ret[k] = v
   344  	}
   345  	for k, v := range b {
   346  		ret[k] = v
   347  	}
   348  	return ret
   349  }
   350  
   351  func (raw *rawConfig) toConfig() *Config {
   352  	ret := EmptyConfig()
   353  	rc := raw.Config
   354  
   355  	if rc != nil {
   356  		if rc.Module != nil {
   357  			ret.Module = *rc.Module
   358  		}
   359  		if rc.DeepCheck != nil {
   360  			ret.DeepCheck = *rc.DeepCheck
   361  		}
   362  		if rc.Force != nil {
   363  			ret.Force = *rc.Force
   364  		}
   365  		if rc.DisabledByDefault != nil {
   366  			ret.DisabledByDefault = *rc.DisabledByDefault
   367  		}
   368  		if rc.AwsCredentials != nil {
   369  			credentials := *rc.AwsCredentials
   370  			ret.AwsCredentials.AccessKey = credentials["access_key"]
   371  			ret.AwsCredentials.SecretKey = credentials["secret_key"]
   372  			ret.AwsCredentials.Profile = credentials["profile"]
   373  			ret.AwsCredentials.CredsFile = credentials["shared_credentials_file"]
   374  			ret.AwsCredentials.Region = credentials["region"]
   375  		}
   376  		if rc.IgnoreModule != nil {
   377  			ret.IgnoreModules = *rc.IgnoreModule
   378  		}
   379  		if rc.Varfile != nil {
   380  			ret.Varfiles = *rc.Varfile
   381  		}
   382  		if rc.Variables != nil {
   383  			ret.Variables = *rc.Variables
   384  		}
   385  	}
   386  
   387  	for _, r := range raw.Rules {
   388  		var rule = r
   389  		ret.Rules[rule.Name] = &rule
   390  	}
   391  
   392  	for _, p := range raw.Plugins {
   393  		var plugin = p
   394  		ret.Plugins[plugin.Name] = &plugin
   395  	}
   396  
   397  	return ret
   398  }