main: allow overriding host-based discovery in CLI config

For situations where the default network-based discovery is inappropriate
or inconvenient, this allows users to provide a hard-coded discovery
document for a particular hostname in the CLI config.

This is a new config block, rather than combined with the existing
"credentials" block, because credentials should ideally live in separate
files from other config so that they can be managed more carefully.
However, this new "host" block _is_ designed to have room for additional
host-specific configuration _other than_ credentials in future, which
might include TLS certificate overrides or other such things used during
the discovery step.
This commit is contained in:
Martin Atkins 2017-10-25 16:00:08 -07:00
parent 74180229d0
commit e9816c60f1
4 changed files with 107 additions and 0 deletions

View File

@ -39,6 +39,15 @@ func initCommands(config *Config) {
credsSrc := credentialsSource(config) credsSrc := credentialsSource(config)
services := disco.NewDisco() services := disco.NewDisco()
services.SetCredentialsSource(credsSrc) services.SetCredentialsSource(credsSrc)
for userHost, hostConfig := range config.Hosts {
host, err := svchost.ForComparison(userHost)
if err != nil {
// We expect the config was already validated by the time we get
// here, so we'll just ignore invalid hostnames.
continue
}
services.ForceHostServices(host, hostConfig.Services)
}
meta := command.Meta{ meta := command.Meta{
Color: true, Color: true,

View File

@ -32,10 +32,19 @@ type Config struct {
// avoid repeatedly re-downloading over the Internet. // avoid repeatedly re-downloading over the Internet.
PluginCacheDir string `hcl:"plugin_cache_dir"` PluginCacheDir string `hcl:"plugin_cache_dir"`
Hosts map[string]*ConfigHost `hcl:"host"`
Credentials map[string]map[string]interface{} `hcl:"credentials"` Credentials map[string]map[string]interface{} `hcl:"credentials"`
CredentialsHelpers map[string]*ConfigCredentialsHelper `hcl:"credentials_helper"` 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" // ConfigCredentialsHelper is the structure of the "credentials_helper"
// nested block within the CLI configuration. // nested block within the CLI configuration.
type ConfigCredentialsHelper struct { type ConfigCredentialsHelper struct {
@ -204,6 +213,16 @@ func (c *Config) Validate() tfdiags.Diagnostics {
// to give proper source references to any errors. We should improve // to give proper source references to any errors. We should improve
// on this when we change the CLI config parser to use HCL2. // 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. // Check that all "credentials" blocks have valid hostnames.
for givenHost := range c.Credentials { for givenHost := range c.Credentials {
_, err := svchost.ForComparison(givenHost) _, err := svchost.ForComparison(givenHost)
@ -256,6 +275,16 @@ func (c1 *Config) Merge(c2 *Config) *Config {
result.PluginCacheDir = c2.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 { if (len(c1.Credentials) + len(c2.Credentials)) > 0 {
result.Credentials = make(map[string]map[string]interface{}) result.Credentials = make(map[string]map[string]interface{})
for host, creds := range c1.Credentials { for host, creds := range c1.Credentials {

View File

@ -54,6 +54,27 @@ func TestLoadConfig_env(t *testing.T) {
} }
} }
func TestLoadConfig_hosts(t *testing.T) {
got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts"))
if len(diags) != 0 {
t.Fatalf("%s", diags.Err())
}
want := &Config{
Hosts: map[string]*ConfigHost{
"example.com": {
Services: map[string]interface{}{
"modules.v1": "https://example.com/",
},
},
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want))
}
}
func TestLoadConfig_credentials(t *testing.T) { func TestLoadConfig_credentials(t *testing.T) {
got, err := loadConfigFile(filepath.Join(fixtureDir, "credentials")) got, err := loadConfigFile(filepath.Join(fixtureDir, "credentials"))
if err != nil { if err != nil {
@ -95,6 +116,22 @@ func TestConfigValidate(t *testing.T) {
&Config{}, &Config{},
0, 0,
}, },
"host good": {
&Config{
Hosts: map[string]*ConfigHost{
"example.com": {},
},
},
0,
},
"host with bad hostname": {
&Config{
Hosts: map[string]*ConfigHost{
"example..com": {},
},
},
1, // host block has invalid hostname
},
"credentials good": { "credentials good": {
&Config{ &Config{
Credentials: map[string]map[string]interface{}{ Credentials: map[string]map[string]interface{}{
@ -157,6 +194,13 @@ func TestConfig_Merge(t *testing.T) {
"local": "local", "local": "local",
"remote": "bad", "remote": "bad",
}, },
Hosts: map[string]*ConfigHost{
"example.com": {
Services: map[string]interface{}{
"modules.v1": "http://example.com/",
},
},
},
Credentials: map[string]map[string]interface{}{ Credentials: map[string]map[string]interface{}{
"foo": { "foo": {
"bar": "baz", "bar": "baz",
@ -175,6 +219,13 @@ func TestConfig_Merge(t *testing.T) {
Provisioners: map[string]string{ Provisioners: map[string]string{
"remote": "remote", "remote": "remote",
}, },
Hosts: map[string]*ConfigHost{
"example.net": {
Services: map[string]interface{}{
"modules.v1": "https://example.net/",
},
},
},
Credentials: map[string]map[string]interface{}{ Credentials: map[string]map[string]interface{}{
"fee": { "fee": {
"bur": "bez", "bur": "bez",
@ -195,6 +246,18 @@ func TestConfig_Merge(t *testing.T) {
"local": "local", "local": "local",
"remote": "remote", "remote": "remote",
}, },
Hosts: map[string]*ConfigHost{
"example.com": {
Services: map[string]interface{}{
"modules.v1": "http://example.com/",
},
},
"example.net": {
Services: map[string]interface{}{
"modules.v1": "https://example.net/",
},
},
},
Credentials: map[string]map[string]interface{}{ Credentials: map[string]map[string]interface{}{
"foo": { "foo": {
"bar": "baz", "bar": "baz",

6
test-fixtures/hosts Normal file
View File

@ -0,0 +1,6 @@
host "example.com" {
services = {
"modules.v1" = "https://example.com/",
}
}