diff --git a/command/agent/config.go b/command/agent/config.go index 5a720a77eb04..8fbb08a67360 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -839,7 +839,7 @@ func (mode *devModeConfig) networkConfig() error { return fmt.Errorf(errMsg, err) } if len(ifAddrs) < 1 { - return fmt.Errorf(errMsg, "could not find public network inteface") + return fmt.Errorf(errMsg, "could not find public network interface") } iface := ifAddrs[0].Name mode.iface = iface @@ -1180,7 +1180,7 @@ func (c *Config) Merge(b *Config) *Config { } // normalizeAddrs normalizes Addresses and AdvertiseAddrs to always be -// initialized and have sane defaults. +// initialized and have reasonable defaults. func (c *Config) normalizeAddrs() error { if c.BindAddr != "" { ipStr, err := parseSingleIPTemplate(c.BindAddr) @@ -1235,9 +1235,49 @@ func (c *Config) normalizeAddrs() error { c.AdvertiseAddrs.Serf = addr } + // Skip network_interface evaluation if not a client + if c.Client != nil && c.Client.Enabled && c.Client.NetworkInterface != "" { + parsed, err := parseSingleInterfaceTemplate(c.Client.NetworkInterface) + if err != nil { + return fmt.Errorf("Failed to parse network-interface: %v", err) + } + + c.Client.NetworkInterface = parsed + } + return nil } +// parseSingleInterfaceTemplate parses a go-sockaddr template and returns an +// error if it doesn't result in a single value. +func parseSingleInterfaceTemplate(tpl string) (string, error) { + out, err := template.Parse(tpl) + if err != nil { + // Typically something like: + // unable to parse template "{{printfl \"en50\"}}": template: sockaddr.Parse:1: function "printfl" not defined + return "", err + } + + // Remove any extra empty space around the rendered result and check if the + // result is also not empty if the user provided a template. + out = strings.TrimSpace(out) + if tpl != "" && out == "" { + return "", fmt.Errorf("template %q evaluated to empty result", tpl) + } + + // `template.Parse` returns a space-separated list of results, but on + // Windows network interfaces are allowed to have spaces, so there is no + // guaranteed separators that we can use to test if the template returned + // multiple interfaces. + // The test below checks if the template results to a single valid interface. + _, err = net.InterfaceByName(out) + if err != nil { + return "", fmt.Errorf("invalid interface name %q", out) + } + + return out, nil +} + // parseSingleIPTemplate is used as a helper function to parse out a single IP // address from a config parameter. func parseSingleIPTemplate(ipTmpl string) (string, error) { diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 11681f002d3e..0233a510644f 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + sockaddr "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/nomad/client/testutil" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper/freeport" @@ -986,6 +987,103 @@ func TestConfig_normalizeAddrs(t *testing.T) { } } +func TestConfig_templateNetworkInterface(t *testing.T) { + // find the first interface + ifaces, err := sockaddr.GetAllInterfaces() + if err != nil { + t.Fatalf("failed to get interfaces: %v", err) + } + iface := ifaces[0] + testCases := []struct { + name string + clientConfig *ClientConfig + expectedInterface string + expectErr bool + }{ + { + name: "empty string", + clientConfig: &ClientConfig{ + Enabled: true, + NetworkInterface: "", + }, + expectedInterface: "", + expectErr: false, + }, + { + name: "simple string", + clientConfig: &ClientConfig{ + Enabled: true, + NetworkInterface: iface.Name, + }, + expectedInterface: iface.Name, + expectErr: false, + }, + { + name: "valid interface", + clientConfig: &ClientConfig{ + Enabled: true, + NetworkInterface: `{{ GetAllInterfaces | attr "name" }}`, + }, + expectedInterface: iface.Name, + expectErr: false, + }, + { + name: "invalid interface", + clientConfig: &ClientConfig{ + Enabled: true, + NetworkInterface: `no such interface`, + }, + expectedInterface: iface.Name, + expectErr: true, + }, + { + name: "insignificant whitespace", + clientConfig: &ClientConfig{ + Enabled: true, + NetworkInterface: ` {{GetAllInterfaces | attr "name" }}`, + }, + expectedInterface: iface.Name, + expectErr: false, + }, + { + name: "empty template return", + clientConfig: &ClientConfig{ + Enabled: true, + NetworkInterface: `{{ printf "" }}`, + }, + expectedInterface: iface.Name, + expectErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := &Config{ + BindAddr: "127.0.0.1", + Ports: &Ports{ + HTTP: 4646, + RPC: 4647, + Serf: 4648, + }, + Addresses: &Addresses{}, + AdvertiseAddrs: &AdvertiseAddrs{ + HTTP: "127.0.0.1:4646", + RPC: "127.0.0.1:4647", + Serf: "127.0.0.1:4648", + }, + DevMode: false, + Client: tc.clientConfig, + } + err := c.normalizeAddrs() + if tc.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, c.Client.NetworkInterface, tc.expectedInterface) + }) + } +} + func TestIsMissingPort(t *testing.T) { _, _, err := net.SplitHostPort("localhost") if missing := isMissingPort(err); !missing { diff --git a/website/content/docs/configuration/client.mdx b/website/content/docs/configuration/client.mdx index b3e2d5f6ad2b..cf5620387911 100644 --- a/website/content/docs/configuration/client.mdx +++ b/website/content/docs/configuration/client.mdx @@ -48,7 +48,8 @@ client { to force network fingerprinting on. When run in dev mode, this defaults to the loopback interface. When not in dev mode, the interface attached to the default route is used. The scheduler chooses from these fingerprinted IP - addresses when allocating ports for tasks. + addresses when allocating ports for tasks. This value support [go-sockaddr/template + format][go-sockaddr/template]. If no non-local IP addresses are found, Nomad could fingerprint link-local IPv6 addresses depending on the client's @@ -455,3 +456,4 @@ client { [server-join]: /docs/configuration/server_join 'Server Join' [metadata_constraint]: /docs/job-specification/constraint#user-specified-metadata 'Nomad User-Specified Metadata Constraint Example' [task working directory]: /docs/runtime/environment#task-directories 'Task directories' +[go-sockaddr/template]: https://godoc.org/github.com/hashicorp/go-sockaddr/template