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/", + } +}