Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement interface specification syntax #58

Merged
merged 12 commits into from
Jan 5, 2016
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ The format of the JSON file configuration is as follows:
"http://localhost/app"
],
"interfaces": [
"eth0"
"eth0",
"eth1[1]",
"192.168.0.0/16",
"2001:db8::/64",
"eth2:inet",
"eth2:inet6",
"inet",
"inet6"
],
"poll": 10,
"ttl": 30
Expand All @@ -85,7 +92,7 @@ Service fields:
- `name` is the name of the service as it will appear in Consul. Each instance of the service will have a unique ID made up from `name`+hostname of the container.
- `port` is the port the service will advertise to Consul.
- `health` is the executable (and its arguments) used to check the health of the service.
- `interfaces` is an optional single interface name or array of interfaces in priority order. If given, the IP of the service will be obtained from the first interface that exits in the container. (Default value is `["eth0"]`)
- `interfaces` is an optional single or array of interface specifications. If given, the IP of the service will be obtained from the first interface specification that matches. (Default value is `["eth0:inet"]`)
- `poll` is the time in seconds between polling for health checks.
- `ttl` is the time-to-live of a successful health check. This should be longer than the polling rate so that the polling process and the TTL aren't racing; otherwise Consul will mark the service as unhealthy.

Expand All @@ -105,6 +112,27 @@ Other fields:

*Note that if you're using `curl` to check HTTP endpoints for health checks, that it doesn't return a non-zero exit code on 404s or similar failure modes by default. Use the `--fail` flag for curl if you need to catch those cases.*

#### Interface Specifications

The `interfaces` parameter allows for one or more specifications to be used when searching for the advertised IP. The first specification that matches stops the search process, so they should be ordered from most specific to least specific.

- `eth0` : Match the first IPv4 address on `eth0` (alias for `eth0:inet`)
- `eth0:inet6` : Match the first IPv6 address on `eth0`
- `eth0[2]` : Match the 2nd IP address on `eth0`
- `10.0.0.0/16` : Match the first IP that is contained within the IP Network
- `fdc6:238c:c4bc::/48` : Match the first IP that is contained within the IPv6 Network
- `inet` : Match the first IPv4 Address (excluding `127.0.0.0/8`)
- `inet6` : Match the first IPv6 Address (excluding `::1/128`)

Interfaces and their IP addresses are ordered alphabetically by interface name, then by IP address (lexicographically by bytes).

**Sample Ordering**

- eth0 10.2.0.1 192.168.1.100
- eth1 10.0.0.100 10.0.0.200
- eth2 10.1.0.200 fdc6:238c:c4bc::1
- lo ::1 127.0.0.1

#### Commands & arguments

All executable fields, such as `onStart` and `onChange`, accept both a string or an array. If a string is given, the command and its arguments are separated by spaces; otherwise, the first element of the array is the command path, and the rest are its arguments.
Expand Down
140 changes: 1 addition & 139 deletions src/containerbuddy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ import (
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"regexp"
"strings"
"sync"
)
Expand Down Expand Up @@ -298,7 +295,7 @@ func initializeConfig(config *Config) (*Config, error) {
return nil, ifaceErr
}

if service.ipAddress, err = getIP(interfaces); err != nil {
if service.ipAddress, err = GetIP(interfaces); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -378,141 +375,6 @@ func highlightError(data []byte, pos int64) (int, int, string) {
return line, int(col), fmt.Sprintf("%s%s%s", prevLine, thisLine, highlight)
}

// determine the IP address of the container
func getIP(interfaceNames []string) (string, error) {

if interfaceNames == nil || len(interfaceNames) == 0 {
// Use a sane default
interfaceNames = []string{"eth0"}
}

interfaces, interfacesErr := net.Interfaces()

if interfacesErr != nil {
return "", interfacesErr
}

interfaceIPs, interfaceIPsErr := getinterfaceIPs(interfaces)

/* We had an error and there were no interfaces returned, this is clearly
* an error state. */
if interfaceIPsErr != nil && len(interfaceIPs) < 1 {
return "", interfaceIPsErr
}
/* We had error(s) and there were interfaces returned, this is potentially
* recoverable. Let's pass on the parsed interfaces and log the error
* state. */
if interfaceIPsErr != nil && len(interfaceIPs) > 0 {
log.Printf("We had a problem reading information about some network "+
"interfaces. If everything works, it is safe to ignore this"+
"message. Details:\n%s\n", interfaceIPsErr)
}

// Find the interface matching the name given
for _, interfaceName := range interfaceNames {
for _, intf := range interfaceIPs {
if interfaceName == intf.Name {
return intf.IP, nil
}
}
}

// Interface not found, return error
return "", fmt.Errorf("Unable to find interfaces %s in %#v",
interfaceNames, interfaceIPs)
}

type interfaceIP struct {
Name string
IP string
}

// Queries the network interfaces on the running machine and returns a list
// of IPs for each interface. Currently, this only returns IPv4 addresses.
func getinterfaceIPs(interfaces []net.Interface) ([]interfaceIP, error) {
var ifaceIPs []interfaceIP
var errors []string

for _, intf := range interfaces {
ipAddrs, addrErr := intf.Addrs()

if addrErr != nil {
errors = append(errors, addrErr.Error())
continue
}

/* As crazy as it may seem, yes you can have an interface that doesn't
* have an IP address assigned. */
if len(ipAddrs) == 0 {
continue
}

/* We ignore aliases for the time being. We assume that that
* authoritative address is the first address returned from the
* interface. */
ifaceIP, parsingErr := parseIPFromAddress(ipAddrs[0], intf)

if parsingErr != nil {
errors = append(errors, parsingErr.Error())
continue
}

ifaceIPs = append(ifaceIPs, ifaceIP)
}

/* If we had any errors parsing interfaces, we accumulate them all and
* then return them so that the caller can decide what they want to do. */
if len(errors) > 0 {
err := fmt.Errorf(strings.Join(errors, "\n"))
println(err.Error())
return ifaceIPs, err
}

return ifaceIPs, nil
}

// Parses an IP and interface name out of the provided address and interface
// objects. We assume that the default IPv4 address will be the first IPv4 address
// to appear in the list of IPs presented for the interface.
func parseIPFromAddress(address net.Addr, intf net.Interface) (interfaceIP, error) {
ips := strings.Split(address.String(), " ")

// In Linux, we will typically see a value like:
// 192.168.0.7/24 fe80::12c3:7bff:fe45:a2ff/64

var ipv4 string
ipv4Regex := "^\\d+\\.\\d+\\.\\d+\\.\\d+.*$"

for _, ip := range ips {
matched, matchErr := regexp.MatchString(ipv4Regex, ip)

if matchErr != nil {
return interfaceIP{}, matchErr
}

if matched {
ipv4 = ip
break
}
}

if len(ipv4) < 1 {
msg := fmt.Sprintf("No parsable IPv4 address was available for "+
"interface: %s", intf.Name)
return interfaceIP{}, errors.New(msg)
}

ipAddr, _, parseErr := net.ParseCIDR(ipv4)

if parseErr != nil {
return interfaceIP{}, parseErr
}

ifaceIP := interfaceIP{Name: intf.Name, IP: ipAddr.String()}

return ifaceIP, nil
}

func argsToCmd(args []string) *exec.Cmd {
if len(args) == 0 {
return nil
Expand Down
Loading