github.com/wata727/tflint@v0.12.2-0.20191013070026-96dd0d36f385/tflint/config.go (about)

     1  package tflint
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  
     9  	"github.com/hashicorp/hcl/v2/gohcl"
    10  	"github.com/hashicorp/hcl/v2/hclparse"
    11  	homedir "github.com/mitchellh/go-homedir"
    12  	"github.com/wata727/tflint/client"
    13  )
    14  
    15  var defaultConfigFile = ".tflint.hcl"
    16  var fallbackConfigFile = "~/.tflint.hcl"
    17  
    18  type rawConfig struct {
    19  	Config *struct {
    20  		Module         *bool              `hcl:"module"`
    21  		DeepCheck      *bool              `hcl:"deep_check"`
    22  		Force          *bool              `hcl:"force"`
    23  		AwsCredentials *map[string]string `hcl:"aws_credentials"`
    24  		IgnoreModule   *map[string]bool   `hcl:"ignore_module"`
    25  		Varfile        *[]string          `hcl:"varfile"`
    26  		Variables      *[]string          `hcl:"variables"`
    27  		// Removed options
    28  		TerraformVersion *string          `hcl:"terraform_version"`
    29  		IgnoreRule       *map[string]bool `hcl:"ignore_rule"`
    30  	} `hcl:"config,block"`
    31  	Rules []RuleConfig `hcl:"rule,block"`
    32  }
    33  
    34  // Config describes the behavior of TFLint
    35  type Config struct {
    36  	Module         bool
    37  	DeepCheck      bool
    38  	Force          bool
    39  	AwsCredentials client.AwsCredentials
    40  	IgnoreModules  map[string]bool
    41  	Varfiles       []string
    42  	Variables      []string
    43  	Rules          map[string]*RuleConfig
    44  }
    45  
    46  // RuleConfig is a TFLint's rule config
    47  type RuleConfig struct {
    48  	Name    string `hcl:"name,label"`
    49  	Enabled bool   `hcl:"enabled"`
    50  }
    51  
    52  // EmptyConfig returns default config
    53  // It is mainly used for testing
    54  func EmptyConfig() *Config {
    55  	return &Config{
    56  		Module:         false,
    57  		DeepCheck:      false,
    58  		Force:          false,
    59  		AwsCredentials: client.AwsCredentials{},
    60  		IgnoreModules:  map[string]bool{},
    61  		Varfiles:       []string{},
    62  		Variables:      []string{},
    63  		Rules:          map[string]*RuleConfig{},
    64  	}
    65  }
    66  
    67  // LoadConfig loads TFLint config from file
    68  // If failed to load the default config file, it tries to load config file under the home directory
    69  // Therefore, if there is no default config file, it will not return an error
    70  func LoadConfig(file string) (*Config, error) {
    71  	log.Printf("[INFO] Load config: %s", file)
    72  	if _, err := os.Stat(file); !os.IsNotExist(err) {
    73  		cfg, err := loadConfigFromFile(file)
    74  		if err != nil {
    75  			log.Printf("[ERROR] %s", err)
    76  			return nil, err
    77  		}
    78  		return cfg, nil
    79  	} else if file != defaultConfigFile {
    80  		log.Printf("[ERROR] %s", err)
    81  		return nil, fmt.Errorf("`%s` is not found", file)
    82  	} else {
    83  		log.Printf("[INFO] Default config file is not found. Ignored")
    84  	}
    85  
    86  	fallback, err := homedir.Expand(fallbackConfigFile)
    87  	if err != nil {
    88  		log.Printf("[ERROR] %s", err)
    89  		return nil, err
    90  	}
    91  
    92  	log.Printf("[INFO] Load fallback config: %s", fallback)
    93  	if _, err := os.Stat(fallback); !os.IsNotExist(err) {
    94  		cfg, err := loadConfigFromFile(fallback)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		return cfg, nil
    99  	}
   100  	log.Printf("[INFO] Fallback config file is not found. Ignored")
   101  
   102  	log.Print("[INFO] Use default config")
   103  	return EmptyConfig(), nil
   104  }
   105  
   106  // Merge returns a merged copy of the two configs
   107  // Since the argument takes precedence, it can be used as overwriting of the config
   108  func (c *Config) Merge(other *Config) *Config {
   109  	ret := c.copy()
   110  
   111  	if other.Module {
   112  		ret.Module = true
   113  	}
   114  	if other.DeepCheck {
   115  		ret.DeepCheck = true
   116  	}
   117  	if other.Force {
   118  		ret.Force = true
   119  	}
   120  
   121  	ret.AwsCredentials = ret.AwsCredentials.Merge(other.AwsCredentials)
   122  	ret.IgnoreModules = mergeBoolMap(ret.IgnoreModules, other.IgnoreModules)
   123  	ret.Varfiles = append(ret.Varfiles, other.Varfiles...)
   124  	ret.Variables = append(ret.Variables, other.Variables...)
   125  
   126  	ret.Rules = mergeRuleMap(ret.Rules, other.Rules)
   127  
   128  	return ret
   129  }
   130  
   131  func (c *Config) copy() *Config {
   132  	ignoreModules := make(map[string]bool)
   133  	for k, v := range c.IgnoreModules {
   134  		ignoreModules[k] = v
   135  	}
   136  
   137  	varfiles := make([]string, len(c.Varfiles))
   138  	copy(varfiles, c.Varfiles)
   139  
   140  	variables := make([]string, len(c.Variables))
   141  	copy(variables, c.Variables)
   142  
   143  	rules := map[string]*RuleConfig{}
   144  	for k, v := range c.Rules {
   145  		rules[k] = &RuleConfig{}
   146  		*rules[k] = *v
   147  	}
   148  
   149  	return &Config{
   150  		Module:         c.Module,
   151  		DeepCheck:      c.DeepCheck,
   152  		Force:          c.Force,
   153  		AwsCredentials: c.AwsCredentials,
   154  		IgnoreModules:  ignoreModules,
   155  		Varfiles:       varfiles,
   156  		Variables:      variables,
   157  		Rules:          rules,
   158  	}
   159  }
   160  
   161  func loadConfigFromFile(file string) (*Config, error) {
   162  	parser := hclparse.NewParser()
   163  
   164  	f, diags := parser.ParseHCLFile(file)
   165  	if diags.HasErrors() {
   166  		return nil, diags
   167  	}
   168  
   169  	var raw rawConfig
   170  	diags = gohcl.DecodeBody(f.Body, nil, &raw)
   171  	if diags.HasErrors() {
   172  		return nil, diags
   173  	}
   174  
   175  	if raw.Config != nil {
   176  		if raw.Config.TerraformVersion != nil {
   177  			return nil, errors.New("`terraform_version` was removed in v0.9.0 because the option is no longer used")
   178  		}
   179  
   180  		if raw.Config.IgnoreRule != nil {
   181  			return nil, errors.New("`ignore_rule` was removed in v0.12.0. Please define `rule` block with `enabled = false` instead")
   182  		}
   183  	}
   184  
   185  	cfg := raw.toConfig()
   186  	log.Printf("[DEBUG] Config loaded")
   187  	log.Printf("[DEBUG]   Module: %t", cfg.Module)
   188  	log.Printf("[DEBUG]   DeepCheck: %t", cfg.DeepCheck)
   189  	log.Printf("[DEBUG]   Force: %t", cfg.Force)
   190  	log.Printf("[DEBUG]   IgnoreModules: %#v", cfg.IgnoreModules)
   191  	log.Printf("[DEBUG]   Varfiles: %#v", cfg.Varfiles)
   192  	log.Printf("[DEBUG]   Variables: %#v", cfg.Variables)
   193  	log.Printf("[DEBUG]   Rules: %#v", cfg.Rules)
   194  
   195  	return raw.toConfig(), nil
   196  }
   197  
   198  func mergeBoolMap(a, b map[string]bool) map[string]bool {
   199  	ret := map[string]bool{}
   200  	for k, v := range a {
   201  		ret[k] = v
   202  	}
   203  	for k, v := range b {
   204  		ret[k] = v
   205  	}
   206  	return ret
   207  }
   208  
   209  func mergeRuleMap(a, b map[string]*RuleConfig) map[string]*RuleConfig {
   210  	ret := map[string]*RuleConfig{}
   211  	for k, v := range a {
   212  		ret[k] = v
   213  	}
   214  	for k, v := range b {
   215  		ret[k] = v
   216  	}
   217  	return ret
   218  }
   219  
   220  func (raw *rawConfig) toConfig() *Config {
   221  	ret := EmptyConfig()
   222  	rc := raw.Config
   223  
   224  	if rc != nil {
   225  		if rc.Module != nil {
   226  			ret.Module = *rc.Module
   227  		}
   228  		if rc.DeepCheck != nil {
   229  			ret.DeepCheck = *rc.DeepCheck
   230  		}
   231  		if rc.Force != nil {
   232  			ret.Force = *rc.Force
   233  		}
   234  		if rc.AwsCredentials != nil {
   235  			credentials := *rc.AwsCredentials
   236  			ret.AwsCredentials.AccessKey = credentials["access_key"]
   237  			ret.AwsCredentials.SecretKey = credentials["secret_key"]
   238  			ret.AwsCredentials.Profile = credentials["profile"]
   239  			ret.AwsCredentials.CredsFile = credentials["shared_credentials_file"]
   240  			ret.AwsCredentials.Region = credentials["region"]
   241  		}
   242  		if rc.IgnoreModule != nil {
   243  			ret.IgnoreModules = *rc.IgnoreModule
   244  		}
   245  		if rc.Varfile != nil {
   246  			ret.Varfiles = *rc.Varfile
   247  		}
   248  		if rc.Variables != nil {
   249  			ret.Variables = *rc.Variables
   250  		}
   251  	}
   252  
   253  	for _, r := range raw.Rules {
   254  		var rule = r
   255  		ret.Rules[rule.Name] = &rule
   256  	}
   257  
   258  	return ret
   259  }