Add support for late binding to IP addresses using go-sockaddr/template

This commit is contained in:
Jonathan Ballet 2017-02-26 23:28:23 +01:00
parent e14c4e3ee4
commit 72b0a7f34d
7 changed files with 560 additions and 53 deletions

View File

@ -2,6 +2,7 @@ package agent
import (
"encoding/base64"
"errors"
"fmt"
"io"
"net"
@ -13,6 +14,8 @@ import (
"strings"
"time"
"github.com/hashicorp/go-sockaddr/template"
client "github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad"
@ -692,22 +695,45 @@ func (c *Config) Merge(b *Config) *Config {
// normalizeAddrs normalizes Addresses and AdvertiseAddrs to always be
// initialized and have sane defaults.
func (c *Config) normalizeAddrs() error {
c.Addresses.HTTP = normalizeBind(c.Addresses.HTTP, c.BindAddr)
c.Addresses.RPC = normalizeBind(c.Addresses.RPC, c.BindAddr)
c.Addresses.Serf = normalizeBind(c.Addresses.Serf, c.BindAddr)
if c.BindAddr != "" {
ipStr, err := parseSingleIPTemplate(c.BindAddr)
if err != nil {
return fmt.Errorf("Bind address resolution failed: %v", err)
}
c.BindAddr = ipStr
}
addr, err := normalizeBind(c.Addresses.HTTP, c.BindAddr)
if err != nil {
return fmt.Errorf("Failed to parse HTTP address: %v", err)
}
c.Addresses.HTTP = addr
addr, err = normalizeBind(c.Addresses.RPC, c.BindAddr)
if err != nil {
return fmt.Errorf("Failed to parse RPC address: %v", err)
}
c.Addresses.RPC = addr
addr, err = normalizeBind(c.Addresses.Serf, c.BindAddr)
if err != nil {
return fmt.Errorf("Failed to parse Serf address: %v", err)
}
c.Addresses.Serf = addr
c.normalizedAddrs = &Addresses{
HTTP: net.JoinHostPort(c.Addresses.HTTP, strconv.Itoa(c.Ports.HTTP)),
RPC: net.JoinHostPort(c.Addresses.RPC, strconv.Itoa(c.Ports.RPC)),
Serf: net.JoinHostPort(c.Addresses.Serf, strconv.Itoa(c.Ports.Serf)),
}
addr, err := normalizeAdvertise(c.AdvertiseAddrs.HTTP, c.Addresses.HTTP, c.Ports.HTTP, c.DevMode)
addr, err = normalizeAdvertise(c.AdvertiseAddrs.HTTP, c.Addresses.HTTP, c.Ports.HTTP)
if err != nil {
return fmt.Errorf("Failed to parse HTTP advertise address: %v", err)
}
c.AdvertiseAddrs.HTTP = addr
addr, err = normalizeAdvertise(c.AdvertiseAddrs.RPC, c.Addresses.RPC, c.Ports.RPC, c.DevMode)
addr, err = normalizeAdvertise(c.AdvertiseAddrs.RPC, c.Addresses.RPC, c.Ports.RPC)
if err != nil {
return fmt.Errorf("Failed to parse RPC advertise address: %v", err)
}
@ -715,7 +741,7 @@ func (c *Config) normalizeAddrs() error {
// Skip serf if server is disabled
if c.Server != nil && c.Server.Enabled {
addr, err = normalizeAdvertise(c.AdvertiseAddrs.Serf, c.Addresses.Serf, c.Ports.Serf, c.DevMode)
addr, err = normalizeAdvertise(c.AdvertiseAddrs.Serf, c.Addresses.Serf, c.Ports.Serf)
if err != nil {
return fmt.Errorf("Failed to parse Serf advertise address: %v", err)
}
@ -725,14 +751,34 @@ func (c *Config) normalizeAddrs() error {
return 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) {
out, err := template.Parse(ipTmpl)
if err != nil {
return "", fmt.Errorf("Unable to parse address template %q: %v", ipTmpl, err)
}
ips := strings.Split(out, " ")
switch len(ips) {
case 0:
return "", errors.New("No addresses found, please configure one.")
case 1:
return ips[0], nil
default:
return "", fmt.Errorf("Multiple addresses found (%q), please configure one.", out)
}
}
// normalizeBind returns a normalized bind address.
//
// If addr is set it is used, if not the default bind address is used.
func normalizeBind(addr, bind string) string {
func normalizeBind(addr, bind string) (string, error) {
if addr == "" {
return bind
return bind, nil
} else {
return parseSingleIPTemplate(addr)
}
return addr
}
// normalizeAdvertise returns a normalized advertise address.
@ -747,61 +793,28 @@ func normalizeBind(addr, bind string) string {
// is resolved and returned with the port.
//
// Loopback is only considered a valid advertise address in dev mode.
func normalizeAdvertise(addr string, bind string, defport int, dev bool) (string, error) {
func normalizeAdvertise(addr string, bind string, defport int) (string, error) {
if addr != "" {
// Default to using manually configured address
_, _, err := net.SplitHostPort(addr)
host, port, err := net.SplitHostPort(addr)
if err != nil {
if !isMissingPort(err) {
return "", fmt.Errorf("Error parsing advertise address %q: %v", addr, err)
}
// missing port, append the default
return net.JoinHostPort(addr, strconv.Itoa(defport)), nil
host = addr
port = strconv.Itoa(defport)
}
return addr, nil
}
// Fallback to bind address first, and then try resolving the local hostname
ips, err := net.LookupIP(bind)
if err != nil {
return "", fmt.Errorf("Error resolving bind address %q: %v", bind, err)
}
// Return the first unicast address
for _, ip := range ips {
if ip.IsLinkLocalUnicast() || ip.IsGlobalUnicast() {
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
if ip.IsLoopback() && dev {
// loopback is fine for dev mode
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
ipStr, err := parseSingleIPTemplate(host)
if err != nil {
return "", fmt.Errorf("Error parsing advertise address template: %v", err)
}
return net.JoinHostPort(ipStr, port), nil
}
// As a last resort resolve the hostname and use it if it's not
// localhost (as localhost is never a sensible default)
host, err := os.Hostname()
if err != nil {
return "", fmt.Errorf("Unable to get hostname to set advertise address: %v", err)
}
ips, err = net.LookupIP(host)
if err != nil {
return "", fmt.Errorf("Error resolving hostname %q for advertise address: %v", host, err)
}
// Return the first unicast address
for _, ip := range ips {
if ip.IsLinkLocalUnicast() || ip.IsGlobalUnicast() {
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
if ip.IsLoopback() && dev {
// loopback is fine for dev mode
return net.JoinHostPort(ip.String(), strconv.Itoa(defport)), nil
}
}
return "", fmt.Errorf("No valid advertise addresses, please set `advertise` manually")
// Fallback to bind address, as it has been resolved before.
return net.JoinHostPort(bind, strconv.Itoa(defport)), nil
}
// isMissingPort returns true if an error is a "missing port" error from

View File

@ -1,6 +1,7 @@
package agent
import (
"fmt"
"io/ioutil"
"net"
"os"
@ -520,6 +521,121 @@ func TestConfig_Listener(t *testing.T) {
}
}
func TestConfig_normalizeAddrs(t *testing.T) {
c := &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{},
DevMode: true,
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
if c.BindAddr != "127.0.0.1" {
t.Fatalf("expected BindAddr 127.0.0.1, got %s", c.BindAddr)
}
if c.normalizedAddrs.HTTP != "127.0.0.1:4646" {
t.Fatalf("expected HTTP address 127.0.0.1:4646, got %s", c.normalizedAddrs.HTTP)
}
if c.normalizedAddrs.RPC != "127.0.0.1:4647" {
t.Fatalf("expected RPC address 127.0.0.1:4647, got %s", c.normalizedAddrs.RPC)
}
if c.normalizedAddrs.Serf != "127.0.0.1:4648" {
t.Fatalf("expected Serf address 127.0.0.1:4648, got %s", c.normalizedAddrs.Serf)
}
if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
t.Fatalf("expected HTTP advertise address 127.0.0.1:4646, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
}
// Client mode, no Serf address defined
if c.AdvertiseAddrs.Serf != "" {
t.Fatalf("expected unset Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
}
c = &Config{
BindAddr: "169.254.1.5",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{
HTTP: "169.254.1.10",
},
AdvertiseAddrs: &AdvertiseAddrs{
RPC: "169.254.1.40",
},
Server: &ServerConfig{
Enabled: true,
},
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
if c.BindAddr != "169.254.1.5" {
t.Fatalf("expected BindAddr 169.254.1.5, got %s", c.BindAddr)
}
if c.AdvertiseAddrs.HTTP != "169.254.1.10:4646" {
t.Fatalf("expected HTTP advertise address 169.254.1.10:4646, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "169.254.1.40:4647" {
t.Fatalf("expected RPC advertise address 169.254.1.40:4647, got %s", c.AdvertiseAddrs.RPC)
}
if c.AdvertiseAddrs.Serf != "169.254.1.5:4648" {
t.Fatalf("expected Serf advertise address 169.254.1.5:4648, got %s", c.AdvertiseAddrs.Serf)
}
c = &Config{
BindAddr: "{{ GetPrivateIP }}",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{},
Server: &ServerConfig{
Enabled: true,
},
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
if c.AdvertiseAddrs.HTTP != fmt.Sprintf("%s:4646", c.BindAddr) {
t.Fatalf("expected HTTP advertise address %s:4646, got %s", c.BindAddr, c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != fmt.Sprintf("%s:4647", c.BindAddr) {
t.Fatalf("expected RPC advertise address %s:4647, got %s", c.BindAddr, c.AdvertiseAddrs.RPC)
}
if c.AdvertiseAddrs.Serf != fmt.Sprintf("%s:4648", c.BindAddr) {
t.Fatalf("expected Serf advertise address %s:4648, got %s", c.BindAddr, c.AdvertiseAddrs.Serf)
}
}
func TestResources_ParseReserved(t *testing.T) {
cases := []struct {
Input string

View File

@ -0,0 +1,2 @@
test::
go test

View File

@ -0,0 +1,6 @@
# sockaddr/template
sockaddr's template library. See
the
[sockaddr/template](https://godoc.org/github.com/hashicorp/go-sockaddr/template)
docs for details on how to use this template.

239
vendor/github.com/hashicorp/go-sockaddr/template/doc.go generated vendored Normal file
View File

@ -0,0 +1,239 @@
/*
Package sockaddr/template provides a text/template interface the SockAddr helper
functions. The primary entry point into the sockaddr/template package is
through its Parse() call. For example:
import (
"fmt"
template "github.com/hashicorp/go-sockaddr/template"
)
results, err := template.Parse(`{{ GetPrivateIP }}`)
if err != nil {
fmt.Errorf("Unable to find a private IP address: %v", err)
}
fmt.Printf("My Private IP address is: %s\n", results)
Below is a list of builtin template functions and details re: their usage. It
is possible to add additional functions by calling ParseIfAddrsTemplate
directly.
In general, the calling convention for this template library is to seed a list
of initial interfaces via one of the Get*Interfaces() calls, then filter, sort,
and extract the necessary attributes for use as string input. This template
interface is primarily geared toward resolving specific values that are only
available at runtime, but can be defined as a heuristic for execution when a
config file is parsed.
All functions, unless noted otherwise, return an array of IfAddr structs making
it possible to `sort`, `filter`, `limit`, seek (via the `offset` function), or
`unique` the list. To extract useful string information, the `attr` and `join`
functions return a single string value. See below for details.
Important note: see the
https://github.com/hashicorp/go-sockaddr/tree/master/cmd/sockaddr utility for
more examples and for a CLI utility to experiment with the template syntax.
`GetAllInterfaces` - Returns an exhaustive set of IfAddr structs available on
the host. `GetAllInterfaces` is the initial input and accessible as the initial
"dot" in the pipeline.
Example:
{{ GetAllInterfaces }}
`GetDefaultInterfaces` - Returns one IfAddr for every IP that is on the
interface containing the default route for the host.
Example:
{{ GetDefaultInterfaces }}
`GetPrivateInterfaces` - Returns one IfAddr for every forwardable IP address
that is included in RFC 6890, is attached to the interface with the default
route, and whose interface is marked as up. NOTE: RFC 6890 is a more exhaustive
version of RFC1918 because it spans IPv4 and IPv6, however it does permit the
inclusion of likely undesired addresses such as multicast, therefore our version
of "private" also filters out non-forwardable addresses.
Example:
{{ GetPrivateInterfaces | include "flags" "up" }}
`GetPublicInterfaces` - Returns a list of IfAddr that do not match RFC 6890, is
attached to the default route, and whose interface is marked as up.
Example:
{{ GetPublicInterfaces | include "flags" "up" }}
`GetPrivateIP` - Helper function that returns a string of the first IP address
from GetPrivateInterfaces.
Example:
{{ GetPrivateIP }}
`GetPublicIP` - Helper function that returns a string of the first IP from
GetPublicInterfaces.
Example:
{{ GetPublicIP }}
`GetInterfaceIP` - Helper function that returns a string of the first IP from
the named interface.
Example:
{{ GetInterfaceIP }}
`sort` - Sorts the IfAddrs result based on its arguments. `sort` takes one
argument, a list of ways to sort its IfAddrs argument. The list of sort
criteria is comma separated (`,`):
- `address`, `+address`: Ascending sort of IfAddrs by Address
- `-address`: Descending sort of IfAddrs by Address
- `name`, `+name`: Ascending sort of IfAddrs by lexical ordering of interface name
- `-name`: Descending sort of IfAddrs by lexical ordering of interface name
- `port`, `+port`: Ascending sort of IfAddrs by port number
- `-port`: Descending sort of IfAddrs by port number
- `private`, `+private`: Ascending sort of IfAddrs with private addresses first
- `-private`: Descending sort IfAddrs with private addresses last
- `size`, `+size`: Ascending sort of IfAddrs by their network size as determined
by their netmask (larger networks first)
- `-size`: Descending sort of IfAddrs by their network size as determined by their
netmask (smaller networks first)
- `type`, `+type`: Ascending sort of IfAddrs by the type of the IfAddr (Unix,
IPv4, then IPv6)
- `-type`: Descending sort of IfAddrs by the type of the IfAddr (IPv6, IPv4, Unix)
Example:
{{ GetPrivateInterfaces | sort "type,size,address" }}
`exclude` and `include`: Filters IfAddrs based on the selector criteria and its
arguments. Both `exclude` and `include` take two arguments. The list of
available filtering criteria is:
- "address": Filter IfAddrs based on a regexp matching the string representation
of the address
- "flag","flags": Filter IfAddrs based on the list of flags specified. Multiple
flags can be passed together using the pipe character (`|`) to create an inclusive
bitmask of flags. The list of flags is included below.
- "name": Filter IfAddrs based on a regexp matching the interface name.
- "network": Filter IfAddrs based on whether a netowkr is included in a given
CIDR. More than one CIDR can be passed in if each network is separated by
the pipe character (`|`).
- "port": Filter IfAddrs based on an exact match of the port number (number must
be expressed as a string)
- "rfc", "rfcs": Filter IfAddrs based on the matching RFC. If more than one RFC
is specified, the list of RFCs can be joined together using the pipe character (`|`).
- "size": Filter IfAddrs based on the exact match of the mask size.
- "type": Filter IfAddrs based on their SockAddr type. Multiple types can be
specified together by using the pipe character (`|`). Valid types include:
`ip`, `ipv4`, `ipv6`, and `unix`.
Example:
{{ GetPrivateInterfaces | exclude "type" "IPv6" | include "flag" "up|forwardable" }}
`unique`: Removes duplicate entries from the IfAddrs list, assuming the list has
already been sorted. `unique` only takes one argument:
- "address": Removes duplicates with the same address
- "name": Removes duplicates with the same interface names
Example:
{{ GetPrivateInterfaces | sort "type,address" | unique "name" }}
`limit`: Reduces the size of the list to the specified value.
Example:
{{ GetPrivateInterfaces | include "flags" "forwardable|up" | limit 1 }}
`offset`: Seeks into the list by the specified value. A negative value can be
used to seek from the end of the list.
Example:
{{ GetPrivateInterfaces | include "flags" "forwardable|up" | offset "-2" | limit 1 }}
`attr`: Extracts a single attribute of the first member of the list and returns
it as a string. `attr` takes a single attribute name. The list of available
attributes is type-specific and shared between `join`. See below for a list of
supported attributes.
Example:
{{ GetPrivateInterfaces | include "flags" "forwardable|up" | attr "address" }}
`join`: Similar to `attr`, `join` extracts all matching attributes of the list
and returns them as a string joined by the separator, the second argument to
`join`. The list of available attributes is type-specific and shared between
`join`.
Example:
{{ GetPrivateInterfaces | include "flags" "forwardable|up" | join "address" " " }}
`exclude` and `include` flags:
- `broadcast`
- `down`: Is the interface down?
- `forwardable`: Is the IP forwardable?
- `global unicast`
- `interface-local multicast`
- `link-local multicast`
- `link-local unicast`
- `loopback`
- `multicast`
- `point-to-point`
- `unspecified`: Is the IfAddr the IPv6 unspecified address?
- `up`: Is the interface up?
Attributes for `attr` and `join`:
SockAddr Type:
- `string`
- `type`
IPAddr Type:
- `address`
- `binary`
- `first_usable`
- `hex`
- `host`
- `last_usable`
- `mask_bits`
- `netmask`
- `network`
- `octets`: Decimal values per byte
- `port`
- `size`: Number of hosts in the network
IPv4Addr Type:
- `broadcast`
- `uint32`: unsigned integer representation of the value
IPv6Addr Type:
- `uint128`: unsigned integer representation of the value
UnixSock Type:
- `path`
*/
package template

View File

@ -0,0 +1,125 @@
package template
import (
"bytes"
"fmt"
"text/template"
"github.com/hashicorp/errwrap"
sockaddr "github.com/hashicorp/go-sockaddr"
)
var (
// SourceFuncs is a map of all top-level functions that generate
// sockaddr data types.
SourceFuncs template.FuncMap
// SortFuncs is a map of all functions used in sorting
SortFuncs template.FuncMap
// FilterFuncs is a map of all functions used in sorting
FilterFuncs template.FuncMap
// HelperFuncs is a map of all functions used in sorting
HelperFuncs template.FuncMap
)
func init() {
SourceFuncs = template.FuncMap{
// GetAllInterfaces - Returns an exhaustive set of IfAddr
// structs available on the host. `GetAllInterfaces` is the
// initial input and accessible as the initial "dot" in the
// pipeline.
"GetAllInterfaces": sockaddr.GetAllInterfaces,
// GetDefaultInterfaces - Returns one IfAddr for every IP that
// is on the interface containing the default route for the
// host.
"GetDefaultInterfaces": sockaddr.GetDefaultInterfaces,
// GetPrivateInterfaces - Returns one IfAddr for every IP that
// matches RFC 6890, are attached to the interface with the
// default route, and are forwardable IP addresses. NOTE: RFC
// 6890 is a more exhaustive version of RFC1918 because it spans
// IPv4 and IPv6, however it doespermit the inclusion of likely
// undesired addresses such as multicast, therefore our
// definition of a "private" address also excludes
// non-forwardable IP addresses (as defined by the IETF).
"GetPrivateInterfaces": sockaddr.GetPrivateInterfaces,
// GetPublicInterfaces - Returns a list of IfAddr that do not
// match RFC 6890, are attached to the default route, and are
// forwardable.
"GetPublicInterfaces": sockaddr.GetPublicInterfaces,
}
SortFuncs = template.FuncMap{
"sort": sockaddr.SortIfBy,
}
FilterFuncs = template.FuncMap{
"exclude": sockaddr.ExcludeIfs,
"include": sockaddr.IncludeIfs,
}
HelperFuncs = template.FuncMap{
// Misc functions that operate on IfAddrs inputs
"attr": sockaddr.IfAttr,
"join": sockaddr.JoinIfAddrs,
"limit": sockaddr.LimitIfAddrs,
"offset": sockaddr.OffsetIfAddrs,
"unique": sockaddr.UniqueIfAddrsBy,
// Return a Private RFC 6890 IP address string that is attached
// to the default route and a forwardable address.
"GetPrivateIP": sockaddr.GetPrivateIP,
// Return a Public RFC 6890 IP address string that is attached
// to the default route and a forwardable address.
"GetPublicIP": sockaddr.GetPublicIP,
// Return the first IP address of the named interface, sorted by
// the largest network size.
"GetInterfaceIP": sockaddr.GetInterfaceIP,
}
}
// Parse parses input as template input using the addresses available on the
// host, then returns the string output if there are no errors.
func Parse(input string) (string, error) {
addrs, err := sockaddr.GetAllInterfaces()
if err != nil {
return "", errwrap.Wrapf("unable to query interface addresses: {{err}}", err)
}
return ParseIfAddrs(input, addrs)
}
// ParseIfAddrs parses input as template input using the IfAddrs inputs, then
// returns the string output if there are no errors.
func ParseIfAddrs(input string, ifAddrs sockaddr.IfAddrs) (string, error) {
return ParseIfAddrsTemplate(input, ifAddrs, template.New("sockaddr.Parse"))
}
// ParseIfAddrsTemplate parses input as template input using the IfAddrs inputs,
// then returns the string output if there are no errors.
func ParseIfAddrsTemplate(input string, ifAddrs sockaddr.IfAddrs, tmplIn *template.Template) (string, error) {
// Create a template, add the function map, and parse the text.
tmpl, err := tmplIn.Option("missingkey=error").
Funcs(SourceFuncs).
Funcs(SortFuncs).
Funcs(FilterFuncs).
Funcs(HelperFuncs).
Parse(input)
if err != nil {
return "", errwrap.Wrapf(fmt.Sprintf("unable to parse template %+q: {{err}}", input), err)
}
var outWriter bytes.Buffer
err = tmpl.Execute(&outWriter, ifAddrs)
if err != nil {
return "", errwrap.Wrapf(fmt.Sprintf("unable to execute sockaddr input %+q: {{err}}", input), err)
}
return outWriter.String(), nil
}

6
vendor/vendor.json vendored
View File

@ -793,6 +793,12 @@
"revision": "f910dd83c2052566cad78352c33af714358d1372",
"revisionTime": "2017-02-08T07:30:35Z"
},
{
"checksumSHA1": "lPzwetgfMBtpHqdTPolgejMctVQ=",
"path": "github.com/hashicorp/go-sockaddr/template",
"revision": "f910dd83c2052566cad78352c33af714358d1372",
"revisionTime": "2017-02-08T07:30:35Z"
},
{
"path": "github.com/hashicorp/go-syslog",
"revision": "42a2b573b664dbf281bd48c3cc12c086b17a39ba"