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 }