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