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 }