github.com/lukahartwig/terraform@v0.11.4-0.20180302171601-664391c254ea/config.go (about)

     1  //go:generate go run ./scripts/generate-plugins.go
     2  package main
     3  
     4  import (
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/hashicorp/hcl"
    12  
    13  	"github.com/hashicorp/terraform/command"
    14  	"github.com/hashicorp/terraform/svchost"
    15  	"github.com/hashicorp/terraform/tfdiags"
    16  )
    17  
    18  const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR"
    19  
    20  // Config is the structure of the configuration for the Terraform CLI.
    21  //
    22  // This is not the configuration for Terraform itself. That is in the
    23  // "config" package.
    24  type Config struct {
    25  	Providers    map[string]string
    26  	Provisioners map[string]string
    27  
    28  	DisableCheckpoint          bool `hcl:"disable_checkpoint"`
    29  	DisableCheckpointSignature bool `hcl:"disable_checkpoint_signature"`
    30  
    31  	// If set, enables local caching of plugins in this directory to
    32  	// avoid repeatedly re-downloading over the Internet.
    33  	PluginCacheDir string `hcl:"plugin_cache_dir"`
    34  
    35  	Hosts map[string]*ConfigHost `hcl:"host"`
    36  
    37  	Credentials        map[string]map[string]interface{}   `hcl:"credentials"`
    38  	CredentialsHelpers map[string]*ConfigCredentialsHelper `hcl:"credentials_helper"`
    39  }
    40  
    41  // ConfigHost is the structure of the "host" nested block within the CLI
    42  // configuration, which can be used to override the default service host
    43  // discovery behavior for a particular hostname.
    44  type ConfigHost struct {
    45  	Services map[string]interface{} `hcl:"services"`
    46  }
    47  
    48  // ConfigCredentialsHelper is the structure of the "credentials_helper"
    49  // nested block within the CLI configuration.
    50  type ConfigCredentialsHelper struct {
    51  	Args []string `hcl:"args"`
    52  }
    53  
    54  // BuiltinConfig is the built-in defaults for the configuration. These
    55  // can be overridden by user configurations.
    56  var BuiltinConfig Config
    57  
    58  // PluginOverrides are paths that override discovered plugins, set from
    59  // the config file.
    60  var PluginOverrides command.PluginOverrides
    61  
    62  // ConfigFile returns the default path to the configuration file.
    63  //
    64  // On Unix-like systems this is the ".terraformrc" file in the home directory.
    65  // On Windows, this is the "terraform.rc" file in the application data
    66  // directory.
    67  func ConfigFile() (string, error) {
    68  	return configFile()
    69  }
    70  
    71  // ConfigDir returns the configuration directory for Terraform.
    72  func ConfigDir() (string, error) {
    73  	return configDir()
    74  }
    75  
    76  // LoadConfig reads the CLI configuration from the various filesystem locations
    77  // and from the environment, returning a merged configuration along with any
    78  // diagnostics (errors and warnings) encountered along the way.
    79  func LoadConfig() (*Config, tfdiags.Diagnostics) {
    80  	var diags tfdiags.Diagnostics
    81  	configVal := BuiltinConfig // copy
    82  	config := &configVal
    83  
    84  	if mainFilename, err := cliConfigFile(); err == nil {
    85  		if _, err := os.Stat(mainFilename); err == nil {
    86  			mainConfig, mainDiags := loadConfigFile(mainFilename)
    87  			diags = diags.Append(mainDiags)
    88  			config = config.Merge(mainConfig)
    89  		}
    90  	}
    91  
    92  	if configDir, err := ConfigDir(); err == nil {
    93  		if info, err := os.Stat(configDir); err == nil && info.IsDir() {
    94  			dirConfig, dirDiags := loadConfigDir(configDir)
    95  			diags = diags.Append(dirDiags)
    96  			config = config.Merge(dirConfig)
    97  		}
    98  	}
    99  
   100  	if envConfig := EnvConfig(); envConfig != nil {
   101  		// envConfig takes precedence
   102  		config = envConfig.Merge(config)
   103  	}
   104  
   105  	diags = diags.Append(config.Validate())
   106  
   107  	return config, diags
   108  }
   109  
   110  // loadConfigFile loads the CLI configuration from ".terraformrc" files.
   111  func loadConfigFile(path string) (*Config, tfdiags.Diagnostics) {
   112  	var diags tfdiags.Diagnostics
   113  	result := &Config{}
   114  
   115  	log.Printf("Loading CLI configuration from %s", path)
   116  
   117  	// Read the HCL file and prepare for parsing
   118  	d, err := ioutil.ReadFile(path)
   119  	if err != nil {
   120  		diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err))
   121  		return result, diags
   122  	}
   123  
   124  	// Parse it
   125  	obj, err := hcl.Parse(string(d))
   126  	if err != nil {
   127  		diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err))
   128  		return result, diags
   129  	}
   130  
   131  	// Build up the result
   132  	if err := hcl.DecodeObject(&result, obj); err != nil {
   133  		diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err))
   134  		return result, diags
   135  	}
   136  
   137  	// Replace all env vars
   138  	for k, v := range result.Providers {
   139  		result.Providers[k] = os.ExpandEnv(v)
   140  	}
   141  	for k, v := range result.Provisioners {
   142  		result.Provisioners[k] = os.ExpandEnv(v)
   143  	}
   144  
   145  	if result.PluginCacheDir != "" {
   146  		result.PluginCacheDir = os.ExpandEnv(result.PluginCacheDir)
   147  	}
   148  
   149  	return result, diags
   150  }
   151  
   152  func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) {
   153  	var diags tfdiags.Diagnostics
   154  	result := &Config{}
   155  
   156  	entries, err := ioutil.ReadDir(path)
   157  	if err != nil {
   158  		diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err))
   159  		return result, diags
   160  	}
   161  
   162  	for _, entry := range entries {
   163  		name := entry.Name()
   164  		// Ignoring errors here because it is used only to indicate pattern
   165  		// syntax errors, and our patterns are hard-coded here.
   166  		hclMatched, _ := filepath.Match("*.tfrc", name)
   167  		jsonMatched, _ := filepath.Match("*.tfrc.json", name)
   168  		if !(hclMatched || jsonMatched) {
   169  			continue
   170  		}
   171  
   172  		filePath := filepath.Join(path, name)
   173  		fileConfig, fileDiags := loadConfigFile(filePath)
   174  		diags = diags.Append(fileDiags)
   175  		result = result.Merge(fileConfig)
   176  	}
   177  
   178  	return result, diags
   179  }
   180  
   181  // EnvConfig returns a Config populated from environment variables.
   182  //
   183  // Any values specified in this config should override those set in the
   184  // configuration file.
   185  func EnvConfig() *Config {
   186  	config := &Config{}
   187  
   188  	if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" {
   189  		// No Expandenv here, because expanding environment variables inside
   190  		// an environment variable would be strange and seems unnecessary.
   191  		// (User can expand variables into the value while setting it using
   192  		// standard shell features.)
   193  		config.PluginCacheDir = envPluginCacheDir
   194  	}
   195  
   196  	return config
   197  }
   198  
   199  // Validate checks for errors in the configuration that cannot be detected
   200  // just by HCL decoding, returning any problems as diagnostics.
   201  //
   202  // On success, the returned diagnostics will return false from the HasErrors
   203  // method. A non-nil diagnostics is not necessarily an error, since it may
   204  // contain just warnings.
   205  func (c *Config) Validate() tfdiags.Diagnostics {
   206  	var diags tfdiags.Diagnostics
   207  
   208  	if c == nil {
   209  		return diags
   210  	}
   211  
   212  	// FIXME: Right now our config parsing doesn't retain enough information
   213  	// to give proper source references to any errors. We should improve
   214  	// on this when we change the CLI config parser to use HCL2.
   215  
   216  	// Check that all "host" blocks have valid hostnames.
   217  	for givenHost := range c.Hosts {
   218  		_, err := svchost.ForComparison(givenHost)
   219  		if err != nil {
   220  			diags = diags.Append(
   221  				fmt.Errorf("The host %q block has an invalid hostname: %s", givenHost, err),
   222  			)
   223  		}
   224  	}
   225  
   226  	// Check that all "credentials" blocks have valid hostnames.
   227  	for givenHost := range c.Credentials {
   228  		_, err := svchost.ForComparison(givenHost)
   229  		if err != nil {
   230  			diags = diags.Append(
   231  				fmt.Errorf("The credentials %q block has an invalid hostname: %s", givenHost, err),
   232  			)
   233  		}
   234  	}
   235  
   236  	// Should have zero or one "credentials_helper" blocks
   237  	if len(c.CredentialsHelpers) > 1 {
   238  		diags = diags.Append(
   239  			fmt.Errorf("No more than one credentials_helper block may be specified"),
   240  		)
   241  	}
   242  
   243  	return diags
   244  }
   245  
   246  // Merge merges two configurations and returns a third entirely
   247  // new configuration with the two merged.
   248  func (c1 *Config) Merge(c2 *Config) *Config {
   249  	var result Config
   250  	result.Providers = make(map[string]string)
   251  	result.Provisioners = make(map[string]string)
   252  	for k, v := range c1.Providers {
   253  		result.Providers[k] = v
   254  	}
   255  	for k, v := range c2.Providers {
   256  		if v1, ok := c1.Providers[k]; ok {
   257  			log.Printf("[INFO] Local %s provider configuration '%s' overrides '%s'", k, v, v1)
   258  		}
   259  		result.Providers[k] = v
   260  	}
   261  	for k, v := range c1.Provisioners {
   262  		result.Provisioners[k] = v
   263  	}
   264  	for k, v := range c2.Provisioners {
   265  		if v1, ok := c1.Provisioners[k]; ok {
   266  			log.Printf("[INFO] Local %s provisioner configuration '%s' overrides '%s'", k, v, v1)
   267  		}
   268  		result.Provisioners[k] = v
   269  	}
   270  	result.DisableCheckpoint = c1.DisableCheckpoint || c2.DisableCheckpoint
   271  	result.DisableCheckpointSignature = c1.DisableCheckpointSignature || c2.DisableCheckpointSignature
   272  
   273  	result.PluginCacheDir = c1.PluginCacheDir
   274  	if result.PluginCacheDir == "" {
   275  		result.PluginCacheDir = c2.PluginCacheDir
   276  	}
   277  
   278  	if (len(c1.Hosts) + len(c2.Hosts)) > 0 {
   279  		result.Hosts = make(map[string]*ConfigHost)
   280  		for name, host := range c1.Hosts {
   281  			result.Hosts[name] = host
   282  		}
   283  		for name, host := range c2.Hosts {
   284  			result.Hosts[name] = host
   285  		}
   286  	}
   287  
   288  	if (len(c1.Credentials) + len(c2.Credentials)) > 0 {
   289  		result.Credentials = make(map[string]map[string]interface{})
   290  		for host, creds := range c1.Credentials {
   291  			result.Credentials[host] = creds
   292  		}
   293  		for host, creds := range c2.Credentials {
   294  			// We just clobber an entry from the other file right now. Will
   295  			// improve on this later using the more-robust merging behavior
   296  			// built in to HCL2.
   297  			result.Credentials[host] = creds
   298  		}
   299  	}
   300  
   301  	if (len(c1.CredentialsHelpers) + len(c2.CredentialsHelpers)) > 0 {
   302  		result.CredentialsHelpers = make(map[string]*ConfigCredentialsHelper)
   303  		for name, helper := range c1.CredentialsHelpers {
   304  			result.CredentialsHelpers[name] = helper
   305  		}
   306  		for name, helper := range c2.CredentialsHelpers {
   307  			result.CredentialsHelpers[name] = helper
   308  		}
   309  	}
   310  
   311  	return &result
   312  }