github.com/johandry/terraform@v0.11.12-beta1/config.go (about) 1 //go:generate go run ./scripts/generate-plugins.go 2 package main 3 4 import ( 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "path/filepath" 10 11 "github.com/hashicorp/hcl" 12 13 "github.com/hashicorp/terraform/command" 14 "github.com/hashicorp/terraform/svchost" 15 "github.com/hashicorp/terraform/tfdiags" 16 ) 17 18 const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR" 19 20 // Config is the structure of the configuration for the Terraform CLI. 21 // 22 // This is not the configuration for Terraform itself. That is in the 23 // "config" package. 24 type Config struct { 25 Providers map[string]string 26 Provisioners map[string]string 27 28 DisableCheckpoint bool `hcl:"disable_checkpoint"` 29 DisableCheckpointSignature bool `hcl:"disable_checkpoint_signature"` 30 31 // If set, enables local caching of plugins in this directory to 32 // avoid repeatedly re-downloading over the Internet. 33 PluginCacheDir string `hcl:"plugin_cache_dir"` 34 35 Hosts map[string]*ConfigHost `hcl:"host"` 36 37 Credentials map[string]map[string]interface{} `hcl:"credentials"` 38 CredentialsHelpers map[string]*ConfigCredentialsHelper `hcl:"credentials_helper"` 39 } 40 41 // ConfigHost is the structure of the "host" nested block within the CLI 42 // configuration, which can be used to override the default service host 43 // discovery behavior for a particular hostname. 44 type ConfigHost struct { 45 Services map[string]interface{} `hcl:"services"` 46 } 47 48 // ConfigCredentialsHelper is the structure of the "credentials_helper" 49 // nested block within the CLI configuration. 50 type ConfigCredentialsHelper struct { 51 Args []string `hcl:"args"` 52 } 53 54 // BuiltinConfig is the built-in defaults for the configuration. These 55 // can be overridden by user configurations. 56 var BuiltinConfig Config 57 58 // PluginOverrides are paths that override discovered plugins, set from 59 // the config file. 60 var PluginOverrides command.PluginOverrides 61 62 // ConfigFile returns the default path to the configuration file. 63 // 64 // On Unix-like systems this is the ".terraformrc" file in the home directory. 65 // On Windows, this is the "terraform.rc" file in the application data 66 // directory. 67 func ConfigFile() (string, error) { 68 return configFile() 69 } 70 71 // ConfigDir returns the configuration directory for Terraform. 72 func ConfigDir() (string, error) { 73 return configDir() 74 } 75 76 // LoadConfig reads the CLI configuration from the various filesystem locations 77 // and from the environment, returning a merged configuration along with any 78 // diagnostics (errors and warnings) encountered along the way. 79 func LoadConfig() (*Config, tfdiags.Diagnostics) { 80 var diags tfdiags.Diagnostics 81 configVal := BuiltinConfig // copy 82 config := &configVal 83 84 if mainFilename, err := cliConfigFile(); err == nil { 85 if _, err := os.Stat(mainFilename); err == nil { 86 mainConfig, mainDiags := loadConfigFile(mainFilename) 87 diags = diags.Append(mainDiags) 88 config = config.Merge(mainConfig) 89 } 90 } 91 92 if configDir, err := ConfigDir(); err == nil { 93 if info, err := os.Stat(configDir); err == nil && info.IsDir() { 94 dirConfig, dirDiags := loadConfigDir(configDir) 95 diags = diags.Append(dirDiags) 96 config = config.Merge(dirConfig) 97 } 98 } 99 100 if envConfig := EnvConfig(); envConfig != nil { 101 // envConfig takes precedence 102 config = envConfig.Merge(config) 103 } 104 105 diags = diags.Append(config.Validate()) 106 107 return config, diags 108 } 109 110 // loadConfigFile loads the CLI configuration from ".terraformrc" files. 111 func loadConfigFile(path string) (*Config, tfdiags.Diagnostics) { 112 var diags tfdiags.Diagnostics 113 result := &Config{} 114 115 log.Printf("Loading CLI configuration from %s", path) 116 117 // Read the HCL file and prepare for parsing 118 d, err := ioutil.ReadFile(path) 119 if err != nil { 120 diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err)) 121 return result, diags 122 } 123 124 // Parse it 125 obj, err := hcl.Parse(string(d)) 126 if err != nil { 127 diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err)) 128 return result, diags 129 } 130 131 // Build up the result 132 if err := hcl.DecodeObject(&result, obj); err != nil { 133 diags = diags.Append(fmt.Errorf("Error parsing %s: %s", path, err)) 134 return result, diags 135 } 136 137 // Replace all env vars 138 for k, v := range result.Providers { 139 result.Providers[k] = os.ExpandEnv(v) 140 } 141 for k, v := range result.Provisioners { 142 result.Provisioners[k] = os.ExpandEnv(v) 143 } 144 145 if result.PluginCacheDir != "" { 146 result.PluginCacheDir = os.ExpandEnv(result.PluginCacheDir) 147 } 148 149 return result, diags 150 } 151 152 func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) { 153 var diags tfdiags.Diagnostics 154 result := &Config{} 155 156 entries, err := ioutil.ReadDir(path) 157 if err != nil { 158 diags = diags.Append(fmt.Errorf("Error reading %s: %s", path, err)) 159 return result, diags 160 } 161 162 for _, entry := range entries { 163 name := entry.Name() 164 // Ignoring errors here because it is used only to indicate pattern 165 // syntax errors, and our patterns are hard-coded here. 166 hclMatched, _ := filepath.Match("*.tfrc", name) 167 jsonMatched, _ := filepath.Match("*.tfrc.json", name) 168 if !(hclMatched || jsonMatched) { 169 continue 170 } 171 172 filePath := filepath.Join(path, name) 173 fileConfig, fileDiags := loadConfigFile(filePath) 174 diags = diags.Append(fileDiags) 175 result = result.Merge(fileConfig) 176 } 177 178 return result, diags 179 } 180 181 // EnvConfig returns a Config populated from environment variables. 182 // 183 // Any values specified in this config should override those set in the 184 // configuration file. 185 func EnvConfig() *Config { 186 config := &Config{} 187 188 if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" { 189 // No Expandenv here, because expanding environment variables inside 190 // an environment variable would be strange and seems unnecessary. 191 // (User can expand variables into the value while setting it using 192 // standard shell features.) 193 config.PluginCacheDir = envPluginCacheDir 194 } 195 196 return config 197 } 198 199 // Validate checks for errors in the configuration that cannot be detected 200 // just by HCL decoding, returning any problems as diagnostics. 201 // 202 // On success, the returned diagnostics will return false from the HasErrors 203 // method. A non-nil diagnostics is not necessarily an error, since it may 204 // contain just warnings. 205 func (c *Config) Validate() tfdiags.Diagnostics { 206 var diags tfdiags.Diagnostics 207 208 if c == nil { 209 return diags 210 } 211 212 // FIXME: Right now our config parsing doesn't retain enough information 213 // to give proper source references to any errors. We should improve 214 // on this when we change the CLI config parser to use HCL2. 215 216 // Check that all "host" blocks have valid hostnames. 217 for givenHost := range c.Hosts { 218 _, err := svchost.ForComparison(givenHost) 219 if err != nil { 220 diags = diags.Append( 221 fmt.Errorf("The host %q block has an invalid hostname: %s", givenHost, err), 222 ) 223 } 224 } 225 226 // Check that all "credentials" blocks have valid hostnames. 227 for givenHost := range c.Credentials { 228 _, err := svchost.ForComparison(givenHost) 229 if err != nil { 230 diags = diags.Append( 231 fmt.Errorf("The credentials %q block has an invalid hostname: %s", givenHost, err), 232 ) 233 } 234 } 235 236 // Should have zero or one "credentials_helper" blocks 237 if len(c.CredentialsHelpers) > 1 { 238 diags = diags.Append( 239 fmt.Errorf("No more than one credentials_helper block may be specified"), 240 ) 241 } 242 243 return diags 244 } 245 246 // Merge merges two configurations and returns a third entirely 247 // new configuration with the two merged. 248 func (c1 *Config) Merge(c2 *Config) *Config { 249 var result Config 250 result.Providers = make(map[string]string) 251 result.Provisioners = make(map[string]string) 252 for k, v := range c1.Providers { 253 result.Providers[k] = v 254 } 255 for k, v := range c2.Providers { 256 if v1, ok := c1.Providers[k]; ok { 257 log.Printf("[INFO] Local %s provider configuration '%s' overrides '%s'", k, v, v1) 258 } 259 result.Providers[k] = v 260 } 261 for k, v := range c1.Provisioners { 262 result.Provisioners[k] = v 263 } 264 for k, v := range c2.Provisioners { 265 if v1, ok := c1.Provisioners[k]; ok { 266 log.Printf("[INFO] Local %s provisioner configuration '%s' overrides '%s'", k, v, v1) 267 } 268 result.Provisioners[k] = v 269 } 270 result.DisableCheckpoint = c1.DisableCheckpoint || c2.DisableCheckpoint 271 result.DisableCheckpointSignature = c1.DisableCheckpointSignature || c2.DisableCheckpointSignature 272 273 result.PluginCacheDir = c1.PluginCacheDir 274 if result.PluginCacheDir == "" { 275 result.PluginCacheDir = c2.PluginCacheDir 276 } 277 278 if (len(c1.Hosts) + len(c2.Hosts)) > 0 { 279 result.Hosts = make(map[string]*ConfigHost) 280 for name, host := range c1.Hosts { 281 result.Hosts[name] = host 282 } 283 for name, host := range c2.Hosts { 284 result.Hosts[name] = host 285 } 286 } 287 288 if (len(c1.Credentials) + len(c2.Credentials)) > 0 { 289 result.Credentials = make(map[string]map[string]interface{}) 290 for host, creds := range c1.Credentials { 291 result.Credentials[host] = creds 292 } 293 for host, creds := range c2.Credentials { 294 // We just clobber an entry from the other file right now. Will 295 // improve on this later using the more-robust merging behavior 296 // built in to HCL2. 297 result.Credentials[host] = creds 298 } 299 } 300 301 if (len(c1.CredentialsHelpers) + len(c2.CredentialsHelpers)) > 0 { 302 result.CredentialsHelpers = make(map[string]*ConfigCredentialsHelper) 303 for name, helper := range c1.CredentialsHelpers { 304 result.CredentialsHelpers[name] = helper 305 } 306 for name, helper := range c2.CredentialsHelpers { 307 result.CredentialsHelpers[name] = helper 308 } 309 } 310 311 return &result 312 }