diff --git a/command/cliconfig/cliconfig.go b/command/cliconfig/cliconfig.go new file mode 100644 index 000000000..a3e6ea9cf --- /dev/null +++ b/command/cliconfig/cliconfig.go @@ -0,0 +1,354 @@ +// Package cliconfig has the types representing and the logic to load CLI-level +// configuration settings. +// +// The CLI config is a small collection of settings that a user can override via +// some files in their home directory or, in some cases, via environment +// variables. The CLI config is not the same thing as a Terraform configuration +// written in the Terraform language; the logic for those lives in the top-level +// directory "configs". +package cliconfig + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + + "github.com/hashicorp/hcl" + + "github.com/hashicorp/terraform/command" + "github.com/hashicorp/terraform/svchost" + "github.com/hashicorp/terraform/tfdiags" +) + +const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR" + +// Config is the structure of the configuration for the Terraform CLI. +// +// This is not the configuration for Terraform itself. That is in the +// "config" package. +type Config struct { + Providers map[string]string + Provisioners map[string]string + + DisableCheckpoint bool `hcl:"disable_checkpoint"` + DisableCheckpointSignature bool `hcl:"disable_checkpoint_signature"` + + // If set, enables local caching of plugins in this directory to + // avoid repeatedly re-downloading over the Internet. + PluginCacheDir string `hcl:"plugin_cache_dir"` + + Hosts map[string]*ConfigHost `hcl:"host"` + + Credentials map[string]map[string]interface{} `hcl:"credentials"` + CredentialsHelpers map[string]*ConfigCredentialsHelper `hcl:"credentials_helper"` +} + +// ConfigHost is the structure of the "host" nested block within the CLI +// configuration, which can be used to override the default service host +// discovery behavior for a particular hostname. +type ConfigHost struct { + Services map[string]interface{} `hcl:"services"` +} + +// ConfigCredentialsHelper is the structure of the "credentials_helper" +// nested block within the CLI configuration. +type ConfigCredentialsHelper struct { + Args []string `hcl:"args"` +} + +// BuiltinConfig is the built-in defaults for the configuration. These +// can be overridden by user configurations. +var BuiltinConfig Config + +// PluginOverrides are paths that override discovered plugins, set from +// the config file. +var PluginOverrides command.PluginOverrides + +// ConfigFile returns the default path to the configuration file. +// +// On Unix-like systems this is the ".terraformrc" file in the home directory. +// On Windows, this is the "terraform.rc" file in the application data +// directory. +func ConfigFile() (string, error) { + return configFile() +} + +// ConfigDir returns the configuration directory for Terraform. +func ConfigDir() (string, error) { + return configDir() +} + +// LoadConfig reads the CLI configuration from the various filesystem locations +// and from the environment, returning a merged configuration along with any +// diagnostics (errors and warnings) encountered along the way. +func LoadConfig() (*Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + configVal := BuiltinConfig // copy + config := &configVal + + if mainFilename, err := cliConfigFile(); err == nil { + if _, err := os.Stat(mainFilename); err == nil { + mainConfig, mainDiags := loadConfigFile(mainFilename) + diags = diags.Append(mainDiags) + config = config.Merge(mainConfig) + } + } + + if configDir, err := ConfigDir(); err == nil { + if info, err := os.Stat(configDir); err == nil && info.IsDir() { + dirConfig, dirDiags := loadConfigDir(configDir) + diags = diags.Append(dirDiags) + config = config.Merge(dirConfig) + } + } + + if envConfig := EnvConfig(); envConfig != nil { + // envConfig takes precedence + config = envConfig.Merge(config) + } + + diags = diags.Append(config.Validate()) + + return config, diags +} + +// loadConfigFile loads the CLI configuration from ".terraformrc" files. +func loadConfigFile(path string) (*Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + result := &Config{} + + log.Printf("Loading CLI configuration from %s", path) + + // Read the HCL file and prepare for parsing + d, err := ioutil.ReadFile(path) + if err != nil { + diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err)) + return result, diags + } + + // Parse it + obj, err := hcl.Parse(string(d)) + if err != nil { + diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err)) + return result, diags + } + + // Build up the result + if err := hcl.DecodeObject(&result, obj); err != nil { + diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err)) + return result, diags + } + + // Replace all env vars + for k, v := range result.Providers { + result.Providers[k] = os.ExpandEnv(v) + } + for k, v := range result.Provisioners { + result.Provisioners[k] = os.ExpandEnv(v) + } + + if result.PluginCacheDir != "" { + result.PluginCacheDir = os.ExpandEnv(result.PluginCacheDir) + } + + return result, diags +} + +func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + result := &Config{} + + entries, err := ioutil.ReadDir(path) + if err != nil { + diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err)) + return result, diags + } + + for _, entry := range entries { + name := entry.Name() + // Ignoring errors here because it is used only to indicate pattern + // syntax errors, and our patterns are hard-coded here. + hclMatched, _ := filepath.Match("*.tfrc", name) + jsonMatched, _ := filepath.Match("*.tfrc.json", name) + if !(hclMatched || jsonMatched) { + continue + } + + filePath := filepath.Join(path, name) + fileConfig, fileDiags := loadConfigFile(filePath) + diags = diags.Append(fileDiags) + result = result.Merge(fileConfig) + } + + return result, diags +} + +// EnvConfig returns a Config populated from environment variables. +// +// Any values specified in this config should override those set in the +// configuration file. +func EnvConfig() *Config { + config := &Config{} + + if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" { + // No Expandenv here, because expanding environment variables inside + // an environment variable would be strange and seems unnecessary. + // (User can expand variables into the value while setting it using + // standard shell features.) + config.PluginCacheDir = envPluginCacheDir + } + + return config +} + +// Validate checks for errors in the configuration that cannot be detected +// just by HCL decoding, returning any problems as diagnostics. +// +// On success, the returned diagnostics will return false from the HasErrors +// method. A non-nil diagnostics is not necessarily an error, since it may +// contain just warnings. +func (c *Config) Validate() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if c == nil { + return diags + } + + // FIXME: Right now our config parsing doesn't retain enough information + // to give proper source references to any errors. We should improve + // on this when we change the CLI config parser to use HCL2. + + // Check that all "host" blocks have valid hostnames. + for givenHost := range c.Hosts { + _, err := svchost.ForComparison(givenHost) + if err != nil { + diags = diags.Append( + fmt.Errorf("The host %q block has an invalid hostname: %s", givenHost, err), + ) + } + } + + // Check that all "credentials" blocks have valid hostnames. + for givenHost := range c.Credentials { + _, err := svchost.ForComparison(givenHost) + if err != nil { + diags = diags.Append( + fmt.Errorf("The credentials %q block has an invalid hostname: %s", givenHost, err), + ) + } + } + + // Should have zero or one "credentials_helper" blocks + if len(c.CredentialsHelpers) > 1 { + diags = diags.Append( + fmt.Errorf("No more than one credentials_helper block may be specified"), + ) + } + + return diags +} + +// Merge merges two configurations and returns a third entirely +// new configuration with the two merged. +func (c1 *Config) Merge(c2 *Config) *Config { + var result Config + result.Providers = make(map[string]string) + result.Provisioners = make(map[string]string) + for k, v := range c1.Providers { + result.Providers[k] = v + } + for k, v := range c2.Providers { + if v1, ok := c1.Providers[k]; ok { + log.Printf("[INFO] Local %s provider configuration '%s' overrides '%s'", k, v, v1) + } + result.Providers[k] = v + } + for k, v := range c1.Provisioners { + result.Provisioners[k] = v + } + for k, v := range c2.Provisioners { + if v1, ok := c1.Provisioners[k]; ok { + log.Printf("[INFO] Local %s provisioner configuration '%s' overrides '%s'", k, v, v1) + } + result.Provisioners[k] = v + } + result.DisableCheckpoint = c1.DisableCheckpoint || c2.DisableCheckpoint + result.DisableCheckpointSignature = c1.DisableCheckpointSignature || c2.DisableCheckpointSignature + + result.PluginCacheDir = c1.PluginCacheDir + if result.PluginCacheDir == "" { + result.PluginCacheDir = c2.PluginCacheDir + } + + if (len(c1.Hosts) + len(c2.Hosts)) > 0 { + result.Hosts = make(map[string]*ConfigHost) + for name, host := range c1.Hosts { + result.Hosts[name] = host + } + for name, host := range c2.Hosts { + result.Hosts[name] = host + } + } + + if (len(c1.Credentials) + len(c2.Credentials)) > 0 { + result.Credentials = make(map[string]map[string]interface{}) + for host, creds := range c1.Credentials { + result.Credentials[host] = creds + } + for host, creds := range c2.Credentials { + // We just clobber an entry from the other file right now. Will + // improve on this later using the more-robust merging behavior + // built in to HCL2. + result.Credentials[host] = creds + } + } + + if (len(c1.CredentialsHelpers) + len(c2.CredentialsHelpers)) > 0 { + result.CredentialsHelpers = make(map[string]*ConfigCredentialsHelper) + for name, helper := range c1.CredentialsHelpers { + result.CredentialsHelpers[name] = helper + } + for name, helper := range c2.CredentialsHelpers { + result.CredentialsHelpers[name] = helper + } + } + + return &result +} + +func cliConfigFile() (string, error) { + mustExist := true + + configFilePath := os.Getenv("TF_CLI_CONFIG_FILE") + if configFilePath == "" { + configFilePath = os.Getenv("TERRAFORM_CONFIG") + } + + if configFilePath == "" { + var err error + configFilePath, err = ConfigFile() + mustExist = false + + if err != nil { + log.Printf( + "[ERROR] Error detecting default CLI config file path: %s", + err) + } + } + + log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath) + f, err := os.Open(configFilePath) + if err == nil { + f.Close() + return configFilePath, nil + } + + if mustExist || !os.IsNotExist(err) { + return "", err + } + + log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.") + return "", nil +} diff --git a/config_test.go b/command/cliconfig/cliconfig_test.go similarity index 99% rename from config_test.go rename to command/cliconfig/cliconfig_test.go index 34c7a12fc..194296c4c 100644 --- a/config_test.go +++ b/command/cliconfig/cliconfig_test.go @@ -1,4 +1,4 @@ -package main +package cliconfig import ( "os" diff --git a/config_unix.go b/command/cliconfig/config_unix.go similarity index 98% rename from config_unix.go rename to command/cliconfig/config_unix.go index e8ec1a2aa..5922c17ac 100644 --- a/config_unix.go +++ b/command/cliconfig/config_unix.go @@ -1,6 +1,6 @@ // +build !windows -package main +package cliconfig import ( "errors" diff --git a/config_windows.go b/command/cliconfig/config_windows.go similarity index 97% rename from config_windows.go rename to command/cliconfig/config_windows.go index ead48d4b1..526b5d1de 100644 --- a/config_windows.go +++ b/command/cliconfig/config_windows.go @@ -1,6 +1,6 @@ // +build windows -package main +package cliconfig import ( "path/filepath" diff --git a/testdata/config b/command/cliconfig/testdata/config similarity index 100% rename from testdata/config rename to command/cliconfig/testdata/config diff --git a/testdata/config-env b/command/cliconfig/testdata/config-env similarity index 100% rename from testdata/config-env rename to command/cliconfig/testdata/config-env diff --git a/testdata/credentials b/command/cliconfig/testdata/credentials similarity index 100% rename from testdata/credentials rename to command/cliconfig/testdata/credentials diff --git a/testdata/hosts b/command/cliconfig/testdata/hosts similarity index 100% rename from testdata/hosts rename to command/cliconfig/testdata/hosts diff --git a/commands.go b/commands.go index 002409367..3f0785183 100644 --- a/commands.go +++ b/commands.go @@ -25,6 +25,11 @@ var PlumbingCommands map[string]struct{} // Ui is the cli.Ui used for communicating to the outside world. var Ui cli.Ui +// PluginOverrides is set from wrappedMain during configuration processing +// and then eventually passed to the "command" package to specify alternative +// plugin locations via the legacy configuration file mechanism. +var PluginOverrides command.PluginOverrides + const ( ErrorPrefix = "e:" OutputPrefix = "o:" diff --git a/config.go b/config.go index 8c18ef95a..fcb48ffb9 100644 --- a/config.go +++ b/config.go @@ -1,63 +1,36 @@ -//go:generate go run ./scripts/generate-plugins.go package main +// This file has some compatibility aliases/wrappers for functionality that +// has now moved into command/cliconfig . +// +// Don't add anything new here! If new functionality is needed, better to just +// add it in command/cliconfig and then call there directly. + import ( - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - - "github.com/hashicorp/hcl" - - "github.com/hashicorp/terraform/command" - "github.com/hashicorp/terraform/svchost" + "github.com/hashicorp/terraform/command/cliconfig" "github.com/hashicorp/terraform/tfdiags" ) -const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR" +//go:generate go run ./scripts/generate-plugins.go // Config is the structure of the configuration for the Terraform CLI. // // This is not the configuration for Terraform itself. That is in the -// "config" package. -type Config struct { - Providers map[string]string - Provisioners map[string]string - - DisableCheckpoint bool `hcl:"disable_checkpoint"` - DisableCheckpointSignature bool `hcl:"disable_checkpoint_signature"` - - // If set, enables local caching of plugins in this directory to - // avoid repeatedly re-downloading over the Internet. - PluginCacheDir string `hcl:"plugin_cache_dir"` - - Hosts map[string]*ConfigHost `hcl:"host"` - - Credentials map[string]map[string]interface{} `hcl:"credentials"` - CredentialsHelpers map[string]*ConfigCredentialsHelper `hcl:"credentials_helper"` -} +// "configs" package. +type Config = cliconfig.Config // ConfigHost is the structure of the "host" nested block within the CLI // configuration, which can be used to override the default service host // discovery behavior for a particular hostname. -type ConfigHost struct { - Services map[string]interface{} `hcl:"services"` -} +type ConfigHost = cliconfig.ConfigHost // ConfigCredentialsHelper is the structure of the "credentials_helper" // nested block within the CLI configuration. -type ConfigCredentialsHelper struct { - Args []string `hcl:"args"` -} +type ConfigCredentialsHelper = cliconfig.ConfigCredentialsHelper // BuiltinConfig is the built-in defaults for the configuration. These // can be overridden by user configurations. -var BuiltinConfig Config - -// PluginOverrides are paths that override discovered plugins, set from -// the config file. -var PluginOverrides command.PluginOverrides +var BuiltinConfig = cliconfig.BuiltinConfig // ConfigFile returns the default path to the configuration file. // @@ -65,117 +38,19 @@ var PluginOverrides command.PluginOverrides // On Windows, this is the "terraform.rc" file in the application data // directory. func ConfigFile() (string, error) { - return configFile() + return cliconfig.ConfigFile() } // ConfigDir returns the configuration directory for Terraform. func ConfigDir() (string, error) { - return configDir() + return cliconfig.ConfigDir() } // LoadConfig reads the CLI configuration from the various filesystem locations // and from the environment, returning a merged configuration along with any // diagnostics (errors and warnings) encountered along the way. func LoadConfig() (*Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - configVal := BuiltinConfig // copy - config := &configVal - - if mainFilename, err := cliConfigFile(); err == nil { - if _, err := os.Stat(mainFilename); err == nil { - mainConfig, mainDiags := loadConfigFile(mainFilename) - diags = diags.Append(mainDiags) - config = config.Merge(mainConfig) - } - } - - if configDir, err := ConfigDir(); err == nil { - if info, err := os.Stat(configDir); err == nil && info.IsDir() { - dirConfig, dirDiags := loadConfigDir(configDir) - diags = diags.Append(dirDiags) - config = config.Merge(dirConfig) - } - } - - if envConfig := EnvConfig(); envConfig != nil { - // envConfig takes precedence - config = envConfig.Merge(config) - } - - diags = diags.Append(config.Validate()) - - return config, diags -} - -// loadConfigFile loads the CLI configuration from ".terraformrc" files. -func loadConfigFile(path string) (*Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - result := &Config{} - - log.Printf("Loading CLI configuration from %s", path) - - // Read the HCL file and prepare for parsing - d, err := ioutil.ReadFile(path) - if err != nil { - diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err)) - return result, diags - } - - // Parse it - obj, err := hcl.Parse(string(d)) - if err != nil { - diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err)) - return result, diags - } - - // Build up the result - if err := hcl.DecodeObject(&result, obj); err != nil { - diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err)) - return result, diags - } - - // Replace all env vars - for k, v := range result.Providers { - result.Providers[k] = os.ExpandEnv(v) - } - for k, v := range result.Provisioners { - result.Provisioners[k] = os.ExpandEnv(v) - } - - if result.PluginCacheDir != "" { - result.PluginCacheDir = os.ExpandEnv(result.PluginCacheDir) - } - - return result, diags -} - -func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - result := &Config{} - - entries, err := ioutil.ReadDir(path) - if err != nil { - diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err)) - return result, diags - } - - for _, entry := range entries { - name := entry.Name() - // Ignoring errors here because it is used only to indicate pattern - // syntax errors, and our patterns are hard-coded here. - hclMatched, _ := filepath.Match("*.tfrc", name) - jsonMatched, _ := filepath.Match("*.tfrc.json", name) - if !(hclMatched || jsonMatched) { - continue - } - - filePath := filepath.Join(path, name) - fileConfig, fileDiags := loadConfigFile(filePath) - diags = diags.Append(fileDiags) - result = result.Merge(fileConfig) - } - - return result, diags + return cliconfig.LoadConfig() } // EnvConfig returns a Config populated from environment variables. @@ -183,130 +58,5 @@ func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) { // Any values specified in this config should override those set in the // configuration file. func EnvConfig() *Config { - config := &Config{} - - if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" { - // No Expandenv here, because expanding environment variables inside - // an environment variable would be strange and seems unnecessary. - // (User can expand variables into the value while setting it using - // standard shell features.) - config.PluginCacheDir = envPluginCacheDir - } - - return config -} - -// Validate checks for errors in the configuration that cannot be detected -// just by HCL decoding, returning any problems as diagnostics. -// -// On success, the returned diagnostics will return false from the HasErrors -// method. A non-nil diagnostics is not necessarily an error, since it may -// contain just warnings. -func (c *Config) Validate() tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - if c == nil { - return diags - } - - // FIXME: Right now our config parsing doesn't retain enough information - // to give proper source references to any errors. We should improve - // on this when we change the CLI config parser to use HCL2. - - // Check that all "host" blocks have valid hostnames. - for givenHost := range c.Hosts { - _, err := svchost.ForComparison(givenHost) - if err != nil { - diags = diags.Append( - fmt.Errorf("The host %q block has an invalid hostname: %s", givenHost, err), - ) - } - } - - // Check that all "credentials" blocks have valid hostnames. - for givenHost := range c.Credentials { - _, err := svchost.ForComparison(givenHost) - if err != nil { - diags = diags.Append( - fmt.Errorf("The credentials %q block has an invalid hostname: %s", givenHost, err), - ) - } - } - - // Should have zero or one "credentials_helper" blocks - if len(c.CredentialsHelpers) > 1 { - diags = diags.Append( - fmt.Errorf("No more than one credentials_helper block may be specified"), - ) - } - - return diags -} - -// Merge merges two configurations and returns a third entirely -// new configuration with the two merged. -func (c1 *Config) Merge(c2 *Config) *Config { - var result Config - result.Providers = make(map[string]string) - result.Provisioners = make(map[string]string) - for k, v := range c1.Providers { - result.Providers[k] = v - } - for k, v := range c2.Providers { - if v1, ok := c1.Providers[k]; ok { - log.Printf("[INFO] Local %s provider configuration '%s' overrides '%s'", k, v, v1) - } - result.Providers[k] = v - } - for k, v := range c1.Provisioners { - result.Provisioners[k] = v - } - for k, v := range c2.Provisioners { - if v1, ok := c1.Provisioners[k]; ok { - log.Printf("[INFO] Local %s provisioner configuration '%s' overrides '%s'", k, v, v1) - } - result.Provisioners[k] = v - } - result.DisableCheckpoint = c1.DisableCheckpoint || c2.DisableCheckpoint - result.DisableCheckpointSignature = c1.DisableCheckpointSignature || c2.DisableCheckpointSignature - - result.PluginCacheDir = c1.PluginCacheDir - if result.PluginCacheDir == "" { - result.PluginCacheDir = c2.PluginCacheDir - } - - if (len(c1.Hosts) + len(c2.Hosts)) > 0 { - result.Hosts = make(map[string]*ConfigHost) - for name, host := range c1.Hosts { - result.Hosts[name] = host - } - for name, host := range c2.Hosts { - result.Hosts[name] = host - } - } - - if (len(c1.Credentials) + len(c2.Credentials)) > 0 { - result.Credentials = make(map[string]map[string]interface{}) - for host, creds := range c1.Credentials { - result.Credentials[host] = creds - } - for host, creds := range c2.Credentials { - // We just clobber an entry from the other file right now. Will - // improve on this later using the more-robust merging behavior - // built in to HCL2. - result.Credentials[host] = creds - } - } - - if (len(c1.CredentialsHelpers) + len(c2.CredentialsHelpers)) > 0 { - result.CredentialsHelpers = make(map[string]*ConfigCredentialsHelper) - for name, helper := range c1.CredentialsHelpers { - result.CredentialsHelpers[name] = helper - } - for name, helper := range c2.CredentialsHelpers { - result.CredentialsHelpers[name] = helper - } - } - - return &result + return cliconfig.EnvConfig() } diff --git a/main.go b/main.go index c93407805..e75b0e460 100644 --- a/main.go +++ b/main.go @@ -229,41 +229,6 @@ func wrappedMain() int { return exitCode } -func cliConfigFile() (string, error) { - mustExist := true - - configFilePath := os.Getenv("TF_CLI_CONFIG_FILE") - if configFilePath == "" { - configFilePath = os.Getenv("TERRAFORM_CONFIG") - } - - if configFilePath == "" { - var err error - configFilePath, err = ConfigFile() - mustExist = false - - if err != nil { - log.Printf( - "[ERROR] Error detecting default CLI config file path: %s", - err) - } - } - - log.Printf("[DEBUG] Attempting to open CLI config file: %s", configFilePath) - f, err := os.Open(configFilePath) - if err == nil { - f.Close() - return configFilePath, nil - } - - if mustExist || !os.IsNotExist(err) { - return "", err - } - - log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.") - return "", nil -} - // copyOutput uses output prefixes to determine whether data on stdout // should go to stdout or stderr. This is due to panicwrap using stderr // as the log and error channel.