From e9816c60f13a3ac55a2a75f57d3abf7885dd7372 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 25 Oct 2017 16:00:08 -0700 Subject: [PATCH] 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. --- commands.go | 9 +++++++ config.go | 29 +++++++++++++++++++++ config_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++ test-fixtures/hosts | 6 +++++ 4 files changed, 107 insertions(+) create mode 100644 test-fixtures/hosts diff --git a/commands.go b/commands.go index d5e53cf6f..da928ca3c 100644 --- a/commands.go +++ b/commands.go @@ -39,6 +39,15 @@ func initCommands(config *Config) { credsSrc := credentialsSource(config) services := disco.NewDisco() 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{ Color: true, diff --git a/config.go b/config.go index 3c546b876..8c18ef95a 100644 --- a/config.go +++ b/config.go @@ -32,10 +32,19 @@ type Config struct { // avoid repeatedly re-downloading over the Internet. PluginCacheDir string `hcl:"plugin_cache_dir"` + Hosts map[string]*ConfigHost `hcl:"host"` + Credentials map[string]map[string]interface{} `hcl:"credentials"` 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" // nested block within the CLI configuration. type ConfigCredentialsHelper struct { @@ -204,6 +213,16 @@ func (c *Config) Validate() tfdiags.Diagnostics { // to give proper source references to any errors. We should improve // 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. for givenHost := range c.Credentials { _, err := svchost.ForComparison(givenHost) @@ -256,6 +275,16 @@ func (c1 *Config) Merge(c2 *Config) *Config { 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 { result.Credentials = make(map[string]map[string]interface{}) for host, creds := range c1.Credentials { diff --git a/config_test.go b/config_test.go index 479013a50..d48cfddd2 100644 --- a/config_test.go +++ b/config_test.go @@ -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) { got, err := loadConfigFile(filepath.Join(fixtureDir, "credentials")) if err != nil { @@ -95,6 +116,22 @@ func TestConfigValidate(t *testing.T) { &Config{}, 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": { &Config{ Credentials: map[string]map[string]interface{}{ @@ -157,6 +194,13 @@ func TestConfig_Merge(t *testing.T) { "local": "local", "remote": "bad", }, + Hosts: map[string]*ConfigHost{ + "example.com": { + Services: map[string]interface{}{ + "modules.v1": "http://example.com/", + }, + }, + }, Credentials: map[string]map[string]interface{}{ "foo": { "bar": "baz", @@ -175,6 +219,13 @@ func TestConfig_Merge(t *testing.T) { Provisioners: map[string]string{ "remote": "remote", }, + Hosts: map[string]*ConfigHost{ + "example.net": { + Services: map[string]interface{}{ + "modules.v1": "https://example.net/", + }, + }, + }, Credentials: map[string]map[string]interface{}{ "fee": { "bur": "bez", @@ -195,6 +246,18 @@ func TestConfig_Merge(t *testing.T) { "local": "local", "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{}{ "foo": { "bar": "baz", diff --git a/test-fixtures/hosts b/test-fixtures/hosts new file mode 100644 index 000000000..1726404c1 --- /dev/null +++ b/test-fixtures/hosts @@ -0,0 +1,6 @@ + +host "example.com" { + services = { + "modules.v1" = "https://example.com/", + } +}