From 19773e469d2c17077ada6dd590078ec5e7dba5ac Mon Sep 17 00:00:00 2001 From: Charlie Voiselle <464492+angrycub@users.noreply.github.com> Date: Tue, 20 Apr 2021 13:55:10 -0400 Subject: [PATCH] Enable go-sockaddr templating for `network-interface` (#10404) Add templating to `network-interface` option. This PR also adds a fast-fail to in the case where an invalid interface is set or produced by the template * add tests and check for valid interface * Add documentation * Incorporate suggestions from code review Co-authored-by: Luiz Aoqui --- command/agent/config.go | 44 ++++++++- command/agent/config_test.go | 98 +++++++++++++++++++ website/content/docs/configuration/client.mdx | 4 +- 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index 5a720a77eb0..8fbb08a6736 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 11681f002d3..0233a510644 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 b3e2d5f6ad2..cf562038791 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