Skip to content

Commit e1590d0

Browse files
command/cliconfig: Factor out CLI config handling
This is just a wholesale move of the CLI configuration types and functions from the main package into its own package, leaving behind some type aliases and wrappers for now to keep existing callers working. This commit alone doesn't really achieve anything, but in future commits we'll expand the functionality in this package.
1 parent 4692ef1 commit e1590d0

File tree

11 files changed

+379
-305
lines changed

11 files changed

+379
-305
lines changed

command/cliconfig/cliconfig.go

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

config_test.go renamed to command/cliconfig/cliconfig_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package cliconfig
22

33
import (
44
"os"

config_unix.go renamed to command/cliconfig/config_unix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// +build !windows
22

3-
package main
3+
package cliconfig
44

55
import (
66
"errors"

config_windows.go renamed to command/cliconfig/config_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// +build windows
22

3-
package main
3+
package cliconfig
44

55
import (
66
"path/filepath"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

commands.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ var PlumbingCommands map[string]struct{}
2525
// Ui is the cli.Ui used for communicating to the outside world.
2626
var Ui cli.Ui
2727

28+
// PluginOverrides is set from wrappedMain during configuration processing
29+
// and then eventually passed to the "command" package to specify alternative
30+
// plugin locations via the legacy configuration file mechanism.
31+
var PluginOverrides command.PluginOverrides
32+
2833
const (
2934
ErrorPrefix = "e:"
3035
OutputPrefix = "o:"

0 commit comments

Comments
 (0)