diff --git a/src/containerbuddy/config.go b/src/containerbuddy/config.go index 933dfb38..f9bca3ca 100644 --- a/src/containerbuddy/config.go +++ b/src/containerbuddy/config.go @@ -8,9 +8,11 @@ import ( "flag" "fmt" "io/ioutil" + "log" "net" "os" "os/exec" + "regexp" "strings" "sync" ) @@ -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 } @@ -381,7 +404,7 @@ 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 { @@ -389,18 +412,90 @@ type InterfaceIp struct { 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 { diff --git a/src/containerbuddy/config_test.go b/src/containerbuddy/config_test.go index 523b317e..81c52016 100644 --- a/src/containerbuddy/config_test.go +++ b/src/containerbuddy/config_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "flag" + "net" "os" "os/exec" "reflect" @@ -10,6 +11,24 @@ import ( "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", @@ -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