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

Fixes #50. #51

Merged
merged 2 commits into from
Dec 17, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 106 additions & 11 deletions src/containerbuddy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"regexp"
"strings"
"sync"
)
Expand Down Expand Up @@ -368,11 +370,32 @@ func getIp(interfaceNames []string) (string, error) {
// Use a sane default
interfaceNames = []string{"eth0"}
}
interfaces := getInterfaceIps()

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 interfaces {
for _, intf := range interfaceIps {
if interfaceName == intf.Name {
return intf.IP, nil
}
Expand All @@ -381,26 +404,98 @@ func getIp(interfaceNames []string) (string, error) {

// Interface not found, return error
return "", errors.New(fmt.Sprintf("Unable to find interfaces %s in %#v",
interfaceNames, interfaces))
interfaceNames, interfaceIps))
}

type InterfaceIp struct {
Name string
IP string
}

func getInterfaceIps() []InterfaceIp {
// 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
interfaces, _ := net.Interfaces()
var errors []string

for _, intf := range interfaces {
ipAddrs, _ := intf.Addrs()
// We're assuming each interface has one IP here because neither Docker
// nor Triton sets up IP aliasing.
ipAddr, _, _ := net.ParseCIDR(ipAddrs[0].String())
ifaceIp := InterfaceIp{Name: intf.Name, IP: ipAddr.String()}
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)
}
return ifaceIps

/* 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 {
Expand Down
172 changes: 172 additions & 0 deletions src/containerbuddy/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,32 @@ package main
import (
"encoding/json"
"flag"
"net"
"os"
"os/exec"
"reflect"
"strings"
"testing"
)

// ------------------------------------------
// Test setup with mock services

type MockAddr struct {
NetworkAttr string
StringAttr string
}

func (self MockAddr) Network() string {
return self.NetworkAttr
}

func (self MockAddr) String() string {
return self.StringAttr
}

// ------------------------------------------

var testJson = `{
"consul": "consul:8500",
"onStart": "/bin/to/onStart.sh arg1 arg2",
Expand Down Expand Up @@ -305,6 +324,159 @@ func validateParseError(t *testing.T, matchStrings []string, config *Config) {
}
}

func TestInterfaceIpsLoopback(t *testing.T) {
interfaces := make([]net.Interface, 1)

interfaces[0] = net.Interface{
Index: 1,
MTU: 65536,
Name: "lo",
Flags: net.FlagUp | net.FlagLoopback,
}

interfaceIps, err := getInterfaceIps(interfaces)

if err != nil {
t.Error(err)
return
}

/* Because we are testing inside of Docker we can expect that the loopback
* interface to always be on the IPv4 address 127.0.0.1 and to be at
* index 1 */

if len(interfaceIps) != 1 {
t.Error("No IPs were parsed from interface. Expecting: 127.0.0.1")
}

if interfaceIps[0].IP != "127.0.0.1" {
t.Error("Expecting loopback interface [127.0.0.1] to be returned")
}
}

func TestInterfaceIpsError(t *testing.T) {
interfaces := make([]net.Interface, 2)

interfaces[0] = net.Interface{
Index: 1,
MTU: 65536,
Name: "lo",
Flags: net.FlagUp | net.FlagLoopback,
}
interfaces[1] = net.Interface{
Index: -1,
MTU: 65536,
Name: "barf",
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast,
HardwareAddr: []byte{0x10, 0xC3, 0x7B, 0x45, 0xA2, 0xFF},
}

interfaceIps, err := getInterfaceIps(interfaces)

if err != nil {
t.Error(err)
return
}

/* We expect to get only a single valid ip address back because the second
* value is junk. */

if len(interfaceIps) != 1 {
t.Error("No IPs were parsed from interface. Expecting: 127.0.0.1")
}

if interfaceIps[0].IP != "127.0.0.1" {
t.Error("Expecting loopback interface [127.0.0.1] to be returned")
}
}

func TestParseIPv4FromSingleAddress(t *testing.T) {
expectedIp := "192.168.22.123"

intf := net.Interface{
Index: -1,
MTU: 1500,
Name: "fake",
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast,
HardwareAddr: []byte{0x10, 0xC3, 0x7B, 0x45, 0xA2, 0xFF},
}

addr := MockAddr{
NetworkAttr: "ip+net",
StringAttr: expectedIp + "/8",
}

ifaceIp, err := parseIpFromAddress(addr, intf)

if err != nil {
t.Error(err)
return
}

if ifaceIp.IP != expectedIp {
t.Errorf("IP didn't match expectation. Actual: %s Expected: %s",
ifaceIp.IP, expectedIp)
}
}

func TestParseIPv4FromIPv6AndIPv4AddressesIPv4First(t *testing.T) {
expectedIp := "192.168.22.123"

intf := net.Interface{
Index: -1,
MTU: 1500,
Name: "fake",
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast,
HardwareAddr: []byte{0x10, 0xC3, 0x7B, 0x45, 0xA2, 0xFF},
}

addr := MockAddr{
NetworkAttr: "ip+net",
StringAttr: expectedIp + "/8" + " fe80::12c3:7bff:fe45:a2ff/64",
}

ifaceIp, err := parseIpFromAddress(addr, intf)

if err != nil {
t.Error(err)
return
}

if ifaceIp.IP != expectedIp {
t.Errorf("IP didn't match expectation. Actual: %s Expected: %s",
ifaceIp.IP, expectedIp)
}
}

func TestParseIPv4FromIPv6AndIPv4AddressesIPv6First(t *testing.T) {
expectedIp := "192.168.22.123"

intf := net.Interface{
Index: -1,
MTU: 1500,
Name: "fake",
Flags: net.FlagUp | net.FlagBroadcast | net.FlagMulticast,
HardwareAddr: []byte{0x10, 0xC3, 0x7B, 0x45, 0xA2, 0xFF},
}

addr := MockAddr{
NetworkAttr: "ip+net",
StringAttr: "fe80::12c3:7bff:fe45:a2ff/64 " + expectedIp + "/8",
}

ifaceIp, err := parseIpFromAddress(addr, intf)

if err != nil {
t.Error(err)
return
}

if ifaceIp.IP != expectedIp {
t.Errorf("IP didn't match expectation. Actual: %s Expected: %s",
ifaceIp.IP, expectedIp)
}
}

// ----------------------------------------------------
// test helpers

Expand Down