github.com/kevinklinger/open_terraform@v1.3.6/noninternal/command/cliconfig/cliconfig.go (about)

     1  // Package cliconfig has the types representing and the logic to load CLI-level
     2  // configuration settings.
     3  //
     4  // The CLI config is a small collection of settings that a user can override via
     5  // some files in their home directory or, in some cases, via environment
     6  // variables. The CLI config is not the same thing as a Terraform configuration
     7  // written in the Terraform language; the logic for those lives in the top-level
     8  // directory "configs".
     9  package cliconfig
    10  
    11  import (
    12  	"fmt"
    13  	"io/ioutil"
    14  	"log"
    15  	"os"
    16  	"path/filepath"
    17  
    18  	"github.com/hashicorp/hcl"
    19  
    20  	svchost "github.com/hashicorp/terraform-svchost"
    21  	"github.com/kevinklinger/open_terraform/noninternal/tfdiags"
    22  )
    23  
    24  const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR"
    25  
    26  // Config is the structure of the configuration for the Terraform CLI.
    27  //
    28  // This is not the configuration for Terraform itself. That is in the
    29  // "config" package.
    30  type Config struct {
    31  	Providers    map[string]string
    32  	Provisioners map[string]string
    33  
    34  	DisableCheckpoint          bool `hcl:"disable_checkpoint"`
    35  	DisableCheckpointSignature bool `hcl:"disable_checkpoint_signature"`
    36  
    37  	// If set, enables local caching of plugins in this directory to
    38  	// avoid repeatedly re-downloading over the Internet.
    39  	PluginCacheDir string `hcl:"plugin_cache_dir"`
    40  
    41  	Hosts map[string]*ConfigHost `hcl:"host"`
    42  
    43  	Credentials        map[string]map[string]interface{}   `hcl:"credentials"`
    44  	CredentialsHelpers map[string]*ConfigCredentialsHelper `hcl:"credentials_helper"`
    45  
    46  	// ProviderInstallation represents any provider_installation blocks
    47  	// in the configuration. Only one of these is allowed across the whole
    48  	// configuration, but we decode into a slice here so that we can handle
    49  	// that validation at validation time rather than initial decode time.
    50  	ProviderInstallation []*ProviderInstallation
    51  }
    52  
    53  // ConfigHost is the structure of the "host" nested block within the CLI
    54  // configuration, which can be used to override the default service host
    55  // discovery behavior for a particular hostname.
    56  type ConfigHost struct {
    57  	Services map[string]interface{} `hcl:"services"`
    58  }
    59  
    60  // ConfigCredentialsHelper is the structure of the "credentials_helper"
    61  // nested block within the CLI configuration.
    62  type ConfigCredentialsHelper struct {
    63  	Args []string `hcl:"args"`
    64  }
    65  
    66  // BuiltinConfig is the built-in defaults for the configuration. These
    67  // can be overridden by user configurations.
    68  var BuiltinConfig Config
    69  
    70  // ConfigFile returns the default path to the configuration file.
    71  //
    72  // On Unix-like systems this is the ".terraformrc" file in the home directory.
    73  // On Windows, this is the "terraform.rc" file in the application data
    74  // directory.
    75  func ConfigFile() (string, error) {
    76  	return configFile()
    77  }
    78  
    79  // ConfigDir returns the configuration directory for Terraform.
    80  func ConfigDir() (string, error) {
    81  	return configDir()
    82  }
    83  
    84  // LoadConfig reads the CLI configuration from the various filesystem locations
    85  // and from the environment, returning a merged configuration along with any
    86  // diagnostics (errors and warnings) encountered along the way.
    87  func LoadConfig() (*Config, tfdiags.Diagnostics) {
    88  	var diags tfdiags.Diagnostics
    89  	configVal := BuiltinConfig // copy
    90  	config := &configVal
    91  
    92  	if mainFilename, err := cliConfigFile(); err == nil {
    93  		if _, err := os.Stat(mainFilename); err == nil {
    94  			mainConfig, mainDiags := loadConfigFile(mainFilename)
    95  			diags = diags.Append(mainDiags)
    96  			config = config.Merge(mainConfig)
    97  		}
    98  	}
    99  
   100  	// Unless the user has specifically overridden the configuration file
   101  	// location using an environment variable, we'll also load what we find
   102  	// in the config directory. We skip the config directory when source
   103  	// file override is set because we interpret the environment variable
   104  	// being set as an intention to ignore the default set of CLI config
   105  	// files because we're doing something special, like running Terraform
   106  	// in automation with a locally-customized configuration.
   107  	if cliConfigFileOverride() == "" {
   108  		if configDir, err := ConfigDir(); err == nil {
   109  			if info, err := os.Stat(configDir); err == nil && info.IsDir() {
   110  				dirConfig, dirDiags := loadConfigDir(configDir)
   111  				diags = diags.Append(dirDiags)
   112  				config = config.Merge(dirConfig)
   113  			}
   114  		}
   115  	} else {
   116  		log.Printf("[DEBUG] Not reading CLI config directory because config location is overridden by environment variable")
   117  	}
   118  
   119  	if envConfig := EnvConfig(); envConfig != nil {
   120  		// envConfig takes precedence
   121  		config = envConfig.Merge(config)
   122  	}
   123  
   124  	diags = diags.Append(config.Validate())
   125  
   126  	return config, diags
   127  }
   128  
   129  // loadConfigFile loads the CLI configuration from ".terraformrc" files.
   130  func loadConfigFile(path string) (*Config, tfdiags.Diagnostics) {
   131  	var diags tfdiags.Diagnostics
   132  	result := &Config{}
   133  
   134  	log.Printf("Loading CLI configuration from %s", path)
   135  
   136  	// Read the HCL file and prepare for parsing
   137  	d, err := ioutil.ReadFile(path)
   138  	if err != nil {
   139  		diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err))
   140  		return result, diags
   141  	}
   142  
   143  	// Parse it
   144  	obj, err := hcl.Parse(string(d))
   145  	if err != nil {
   146  		diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err))
   147  		return result, diags
   148  	}
   149  
   150  	// Build up the result
   151  	if err := hcl.DecodeObject(&result, obj); err != nil {
   152  		diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err))
   153  		return result, diags
   154  	}
   155  
   156  	// Deal with the provider_installation block, which is not handled using
   157  	// DecodeObject because its structure is not compatible with the
   158  	// limitations of that function.
   159  	providerInstBlocks, moreDiags := decodeProviderInstallationFromConfig(obj)
   160  	diags = diags.Append(moreDiags)
   161  	result.ProviderInstallation = providerInstBlocks
   162  
   163  	// Replace all env vars
   164  	for k, v := range result.Providers {
   165  		result.Providers[k] = os.ExpandEnv(v)
   166  	}
   167  	for k, v := range result.Provisioners {
   168  		result.Provisioners[k] = os.ExpandEnv(v)
   169  	}
   170  
   171  	if result.PluginCacheDir != "" {
   172  		result.PluginCacheDir = os.ExpandEnv(result.PluginCacheDir)
   173  	}
   174  
   175  	return result, diags
   176  }
   177  
   178  func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) {
   179  	var diags tfdiags.Diagnostics
   180  	result := &Config{}
   181  
   182  	entries, err := ioutil.ReadDir(path)
   183  	if err != nil {
   184  		diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err))
   185  		return result, diags
   186  	}
   187  
   188  	for _, entry := range entries {
   189  		name := entry.Name()
   190  		// Ignoring errors here because it is used only to indicate pattern
   191  		// syntax errors, and our patterns are hard-coded here.
   192  		hclMatched, _ := filepath.Match("*.tfrc", name)
   193  		jsonMatched, _ := filepath.Match("*.tfrc.json", name)
   194  		if !(hclMatched || jsonMatched) {
   195  			continue
   196  		}
   197  
   198  		filePath := filepath.Join(path, name)
   199  		fileConfig, fileDiags := loadConfigFile(filePath)
   200  		diags = diags.Append(fileDiags)
   201  		result = result.Merge(fileConfig)
   202  	}
   203  
   204  	return result, diags
   205  }
   206  
   207  // EnvConfig returns a Config populated from environment variables.
   208  //
   209  // Any values specified in this config should override those set in the
   210  // configuration file.
   211  func EnvConfig() *Config {
   212  	config := &Config{}
   213  
   214  	if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" {
   215  		// No Expandenv here, because expanding environment variables inside
   216  		// an environment variable would be strange and seems unnecessary.
   217  		// (User can expand variables into the value while setting it using
   218  		// standard shell features.)
   219  		config.PluginCacheDir = envPluginCacheDir
   220  	}
   221  
   222  	return config
   223  }
   224  
   225  // Validate checks for errors in the configuration that cannot be detected
   226  // just by HCL decoding, returning any problems as diagnostics.
   227  //
   228  // On success, the returned diagnostics will return false from the HasErrors
   229  // method. A non-nil diagnostics is not necessarily an error, since it may
   230  // contain just warnings.
   231  func (c *Config) Validate() tfdiags.Diagnostics {
   232  	var diags tfdiags.Diagnostics
   233  
   234  	if c == nil {
   235  		return diags
   236  	}
   237  
   238  	// FIXME: Right now our config parsing doesn't retain enough information
   239  	// to give proper source references to any errors. We should improve
   240  	// on this when we change the CLI config parser to use HCL2.
   241  
   242  	// Check that all "host" blocks have valid hostnames.
   243  	for givenHost := range c.Hosts {
   244  		_, err := svchost.ForComparison(givenHost)
   245  		if err != nil {
   246  			diags = diags.Append(
   247  				fmt.Errorf("The host %q block has an invalid hostname: %s", givenHost, err),
   248  			)
   249  		}
   250  	}
   251  
   252  	// Check that all "credentials" blocks have valid hostnames.
   253  	for givenHost := range c.Credentials {
   254  		_, err := svchost.ForComparison(givenHost)
   255  		if err != nil {
   256  			diags = diags.Append(
   257  				fmt.Errorf("The credentials %q block has an invalid hostname: %s", givenHost, err),
   258  			)
   259  		}
   260  	}
   261  
   262  	// Should have zero or one "credentials_helper" blocks
   263  	if len(c.CredentialsHelpers) > 1 {
   264  		diags = diags.Append(
   265  			fmt.Errorf("No more than one credentials_helper block may be specified"),
   266  		)
   267  	}
   268  
   269  	// Should have zero or one "provider_installation" blocks
   270  	if len(c.ProviderInstallation) > 1 {
   271  		diags = diags.Append(
   272  			fmt.Errorf("No more than one provider_installation block may be specified"),
   273  		)
   274  	}
   275  
   276  	if c.PluginCacheDir != "" {
   277  		_, err := os.Stat(c.PluginCacheDir)
   278  		if err != nil {
   279  			diags = diags.Append(
   280  				fmt.Errorf("The specified plugin cache dir %s cannot be opened: %s", c.PluginCacheDir, err),
   281  			)
   282  		}
   283  	}
   284  
   285  	return diags
   286  }
   287  
   288  // Merge merges two configurations and returns a third entirely
   289  // new configuration with the two merged.
   290  func (c *Config) Merge(c2 *Config) *Config {
   291  	var result Config
   292  	result.Providers = make(map[string]string)
   293  	result.Provisioners = make(map[string]string)
   294  	for k, v := range c.Providers {
   295  		result.Providers[k] = v
   296  	}
   297  	for k, v := range c2.Providers {
   298  		if v1, ok := c.Providers[k]; ok {
   299  			log.Printf("[INFO] Local %s provider configuration '%s' overrides '%s'", k, v, v1)
   300  		}
   301  		result.Providers[k] = v
   302  	}
   303  	for k, v := range c.Provisioners {
   304  		result.Provisioners[k] = v
   305  	}
   306  	for k, v := range c2.Provisioners {
   307  		if v1, ok := c.Provisioners[k]; ok {
   308  			log.Printf("[INFO] Local %s provisioner configuration '%s' overrides '%s'", k, v, v1)
   309  		}
   310  		result.Provisioners[k] = v
   311  	}
   312  	result.DisableCheckpoint = c.DisableCheckpoint || c2.DisableCheckpoint
   313  	result.DisableCheckpointSignature = c.DisableCheckpointSignature || c2.DisableCheckpointSignature
   314  
   315  	result.PluginCacheDir = c.PluginCacheDir
   316  	if result.PluginCacheDir == "" {
   317  		result.PluginCacheDir = c2.PluginCacheDir
   318  	}
   319  
   320  	if (len(c.Hosts) + len(c2.Hosts)) > 0 {
   321  		result.Hosts = make(map[string]*ConfigHost)
   322  		for name, host := range c.Hosts {
   323  			result.Hosts[name] = host
   324  		}
   325  		for name, host := range c2.Hosts {
   326  			result.Hosts[name] = host
   327  		}
   328  	}
   329  
   330  	if (len(c.Credentials) + len(c2.Credentials)) > 0 {
   331  		result.Credentials = make(map[string]map[string]interface{})
   332  		for host, creds := range c.Credentials {
   333  			result.Credentials[host] = creds
   334  		}
   335  		for host, creds := range c2.Credentials {
   336  			// We just clobber an entry from the other file right now. Will
   337  			// improve on this later using the more-robust merging behavior
   338  			// built in to HCL2.
   339  			result.Credentials[host] = creds
   340  		}
   341  	}
   342  
   343  	if (len(c.CredentialsHelpers) + len(c2.CredentialsHelpers)) > 0 {
   344  		result.CredentialsHelpers = make(map[string]*ConfigCredentialsHelper)
   345  		for name, helper := range c.CredentialsHelpers {
   346  			result.CredentialsHelpers[name] = helper
   347  		}
   348  		for name, helper := range c2.CredentialsHelpers {
   349  			result.CredentialsHelpers[name] = helper
   350  		}
   351  	}
   352  
   353  	if (len(c.ProviderInstallation) + len(c2.ProviderInstallation)) > 0 {
   354  		result.ProviderInstallation = append(result.ProviderInstallation, c.ProviderInstallation...)
   355  		result.ProviderInstallation = append(result.ProviderInstallation, c2.ProviderInstallation...)
   356  	}
   357  
   358  	return &result
   359  }
   360  
   361  func cliConfigFile() (string, error) {
   362  	mustExist := true
   363  
   364  	configFilePath := cliConfigFileOverride()
   365  	if configFilePath == "" {
   366  		var err error
   367  		configFilePath, err = ConfigFile()
   368  		mustExist = false
   369  
   370  		if err != nil {
   371  			log.Printf(
   372  				"[ERROR] Error detecting default CLI config file path: %s",
   373  				err)
   374  		}
   375  	}
   376  
   377  	log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath)
   378  	f, err := os.Open(configFilePath)
   379  	if err == nil {
   380  		f.Close()
   381  		return configFilePath, nil
   382  	}
   383  
   384  	if mustExist || !os.IsNotExist(err) {
   385  		return "", err
   386  	}
   387  
   388  	log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.")
   389  	return "", nil
   390  }
   391  
   392  func cliConfigFileOverride() string {
   393  	configFilePath := os.Getenv("TF_CLI_CONFIG_FILE")
   394  	if configFilePath == "" {
   395  		configFilePath = os.Getenv("TERRAFORM_CONFIG")
   396  	}
   397  	return configFilePath
   398  }