From b6a2b35418996f6e2c69fb6a9af1817735410ab0 Mon Sep 17 00:00:00 2001 From: William Leese Date: Thu, 10 Mar 2016 16:36:16 +0100 Subject: [PATCH 1/9] allow setting the service discovery endpoint Change-Id: Icc088faf4f1718e94afe1834b6cc7633d25cc1c2 --- README.md | 1 + containerbuddy/config.go | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d46b2b0d..43953dc2 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Service fields: - `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 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"]`) +- `endpoint` is an optional field to specify a DNS name of IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. - `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. - `tags` is an optional array of tags. If the discovery service supports it (Consul does), the service will register itself with these tags. diff --git a/containerbuddy/config.go b/containerbuddy/config.go index 1b2ae6a4..2261f03a 100644 --- a/containerbuddy/config.go +++ b/containerbuddy/config.go @@ -8,6 +8,7 @@ import ( "flag" "fmt" "io/ioutil" + "net" "os" "os/exec" "strings" @@ -65,6 +66,7 @@ type ServiceConfig struct { TTL int `json:"ttl"` Interfaces json.RawMessage `json:"interfaces"` Tags []string `json:"tags,omitempty"` + Endpoint string `json:"endpoint,omitempty"` discoveryService DiscoveryService ipAddress string healthCheckCmd *exec.Cmd @@ -311,8 +313,19 @@ func initializeConfig(config *Config) (*Config, error) { return nil, ifaceErr } - if service.ipAddress, err = GetIP(interfaces); err != nil { - return nil, err + if service.Endpoint == "" && service.ipAddress == "" { + if service.ipAddress, err = GetIP(interfaces); err != nil { + return nil, err + } + } else { + if ok := net.ParseIP(service.Endpoint); ok == nil { + if ip, err := net.LookupHost(service.Endpoint); err == nil { + service.ipAddress = ip[0] + } else { + return nil, fmt.Errorf("Could not resolve `Endpoint` in service %s", + service.Name) + } + } } } From 7229acbe4a2fbaaa186588a2575a47a84eb6e4fc Mon Sep 17 00:00:00 2001 From: William Leese Date: Mon, 21 Mar 2016 14:47:31 +0100 Subject: [PATCH 2/9] handle endpoint == ip as well Change-Id: I56d0c8398cf1de800dd15d676dc3d168a5f76237 --- containerbuddy/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/containerbuddy/config.go b/containerbuddy/config.go index 2261f03a..9ef136fc 100644 --- a/containerbuddy/config.go +++ b/containerbuddy/config.go @@ -325,6 +325,8 @@ func initializeConfig(config *Config) (*Config, error) { return nil, fmt.Errorf("Could not resolve `Endpoint` in service %s", service.Name) } + } else { + service.ipAddress = service.Endpoint } } } From 2e7a8602afdc9f70aa89d8cd46d474237aec22f2 Mon Sep 17 00:00:00 2001 From: William Leese Date: Thu, 24 Mar 2016 13:09:33 +0100 Subject: [PATCH 3/9] use address instead of endpoint and forego doing any dns resolving Change-Id: Ie0c85a99d1368f3f334bc9bc3485e7ad3f2ad1ab --- README.md | 2 +- containerbuddy/config.go | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 43953dc2..0aef8183 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Service fields: - `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 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"]`) -- `endpoint` is an optional field to specify a DNS name of IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. +- `address` is an optional field to specify a DNS address or IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. - `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. - `tags` is an optional array of tags. If the discovery service supports it (Consul does), the service will register itself with these tags. diff --git a/containerbuddy/config.go b/containerbuddy/config.go index 9ef136fc..1f72dd91 100644 --- a/containerbuddy/config.go +++ b/containerbuddy/config.go @@ -8,7 +8,6 @@ import ( "flag" "fmt" "io/ioutil" - "net" "os" "os/exec" "strings" @@ -66,7 +65,7 @@ type ServiceConfig struct { TTL int `json:"ttl"` Interfaces json.RawMessage `json:"interfaces"` Tags []string `json:"tags,omitempty"` - Endpoint string `json:"endpoint,omitempty"` + Address string `json:"address,omitempty"` discoveryService DiscoveryService ipAddress string healthCheckCmd *exec.Cmd @@ -313,21 +312,12 @@ func initializeConfig(config *Config) (*Config, error) { return nil, ifaceErr } - if service.Endpoint == "" && service.ipAddress == "" { + if service.Address != "" { + service.ipAddress = service.Address + } else { if service.ipAddress, err = GetIP(interfaces); err != nil { return nil, err } - } else { - if ok := net.ParseIP(service.Endpoint); ok == nil { - if ip, err := net.LookupHost(service.Endpoint); err == nil { - service.ipAddress = ip[0] - } else { - return nil, fmt.Errorf("Could not resolve `Endpoint` in service %s", - service.Name) - } - } else { - service.ipAddress = service.Endpoint - } } } From 67e7662428945b188af02e001036773154b82689 Mon Sep 17 00:00:00 2001 From: William Leese Date: Thu, 7 Apr 2016 13:16:29 +0200 Subject: [PATCH 4/9] Trigger tests Change-Id: I7644677d696aa0b345443e5b35b4a4404212ae19 From 523f1d0bd3ce76722a2b89b9126ebe412dc1c302 Mon Sep 17 00:00:00 2001 From: William Leese Date: Wed, 20 Apr 2016 11:46:56 +0200 Subject: [PATCH 5/9] bring up to speed with master Change-Id: I0613c25de3553324ef8357be4067884188039d4f --- README.md | 1 + services/services.go | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7ca8b17c..6cd71bb5 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ The format of the JSON file configuration is as follows: - `health` is the executable (and its arguments) used to check the health of the service. - `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"]`) - `address` is an optional field to specify a DNS address or IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. +- `address` is an optional field to specify a DNS address or IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. - `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. - `tags` is an optional array of tags. If the discovery service supports it (Consul does), the service will register itself with these tags. diff --git a/services/services.go b/services/services.go index 17c1a411..8f398446 100644 --- a/services/services.go +++ b/services/services.go @@ -19,6 +19,7 @@ type Service struct { Port int `json:"port"` TTL int `json:"ttl"` Interfaces json.RawMessage `json:"interfaces"` + Address string `json:"address,omitempty"` Tags []string `json:"tags,omitempty"` ipAddress string healthCheckCmd *exec.Cmd @@ -88,10 +89,14 @@ func parseService(s *Service, disc discovery.DiscoveryService) error { return ifaceErr } - if ipAddress, err := utils.GetIP(interfaces); err != nil { - return err + if s.Address != "" { + s.ipAddress = s.Address } else { - s.ipAddress = ipAddress + if ipAddress, err := utils.GetIP(interfaces); err != nil { + return err + } else { + s.ipAddress = ipAddress + } } s.definition = &discovery.ServiceDefinition{ ID: s.ID, From c09407792a1f3c9287038126c71ddaa26545e004 Mon Sep 17 00:00:00 2001 From: William Leese Date: Wed, 20 Apr 2016 11:47:35 +0200 Subject: [PATCH 6/9] copy and paste error Change-Id: I39fbe15cbd848b804ab78bf5c1f178d2a01c3acc --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6cd71bb5..7ca8b17c 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,6 @@ The format of the JSON file configuration is as follows: - `health` is the executable (and its arguments) used to check the health of the service. - `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"]`) - `address` is an optional field to specify a DNS address or IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. -- `address` is an optional field to specify a DNS address or IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. - `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. - `tags` is an optional array of tags. If the discovery service supports it (Consul does), the service will register itself with these tags. From 8c4642d81cf6f5b1a87fb574d192a59493c0edbc Mon Sep 17 00:00:00 2001 From: William Leese Date: Thu, 21 Apr 2016 13:31:37 +0200 Subject: [PATCH 7/9] restructure to "interfaces":["static:x.x.x.x"] Change-Id: Ic582a7eeaf3d63ec65d512caa2ee357bb5601cf7 --- README.md | 2 +- services/services.go | 11 +++-------- utils/ips.go | 25 +++++++++++++++++++++++++ utils/ips_test.go | 11 ++++++----- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7ca8b17c..e45ed46a 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,6 @@ The format of the JSON file configuration is as follows: - `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 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"]`) -- `address` is an optional field to specify a DNS address or IP that will override what is obtained from traversing network interfaces. This can be useful in special cases such as during testing or when using bridged networking. - `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. - `tags` is an optional array of tags. If the discovery service supports it (Consul does), the service will register itself with these tags. @@ -225,6 +224,7 @@ The `interfaces` parameter allows for one or more specifications to be used when - `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`) +- `static: 192.168.1.100` : Use this Address. Useful for all cases where the IP is not visible in the container Interfaces and their IP addresses are ordered alphabetically by interface name, then by IP address (lexicographically by bytes). diff --git a/services/services.go b/services/services.go index 8f398446..17c1a411 100644 --- a/services/services.go +++ b/services/services.go @@ -19,7 +19,6 @@ type Service struct { Port int `json:"port"` TTL int `json:"ttl"` Interfaces json.RawMessage `json:"interfaces"` - Address string `json:"address,omitempty"` Tags []string `json:"tags,omitempty"` ipAddress string healthCheckCmd *exec.Cmd @@ -89,14 +88,10 @@ func parseService(s *Service, disc discovery.DiscoveryService) error { return ifaceErr } - if s.Address != "" { - s.ipAddress = s.Address + if ipAddress, err := utils.GetIP(interfaces); err != nil { + return err } else { - if ipAddress, err := utils.GetIP(interfaces); err != nil { - return err - } else { - s.ipAddress = ipAddress - } + s.ipAddress = ipAddress } s.definition = &discovery.ServiceDefinition{ ID: s.ID, diff --git a/utils/ips.go b/utils/ips.go index 18b0f3b2..03359466 100644 --- a/utils/ips.go +++ b/utils/ips.go @@ -88,6 +88,11 @@ func GetIP(specList []string) (string, error) { func findIPWithSpecs(specs []interfaceSpec, interfaceIPs []interfaceIP) (string, error) { // Find the interface matching the name given for _, spec := range specs { + // Static IP given + origSpec, ok := spec.(staticInterfaceSpec) + if ok { + return origSpec.IP.String(), nil + } index := 0 iface := "" for _, iip := range interfaceIPs { @@ -122,6 +127,18 @@ type inetInterfaceSpec struct { IPv6 bool } +// -- matches static +type staticInterfaceSpec struct { + Spec string + Name string + IP net.IP +} + +func (s staticInterfaceSpec) Match(index int, iip interfaceIP) bool { + // Never matches + return false +} + func (s inetInterfaceSpec) Match(index int, iip interfaceIP) bool { if s.Name != "*" && s.Name != iip.Name { return false @@ -187,6 +204,14 @@ func parseInterfaceSpec(spec string) (interfaceSpec, error) { if spec == "inet6" { return inetInterfaceSpec{Spec: spec, Name: "*", IPv6: true}, nil } + if strings.HasPrefix(spec, "static:") { + ip := strings.SplitAfter(spec, "static:") + nip := net.ParseIP(ip[1]) + if nip == nil { + return nil, fmt.Errorf("Unable to parse static ip %s in %s", ip[0], spec) + } + return staticInterfaceSpec{Spec: spec, Name: "static", IP: nip}, nil + } if match := ifaceSpec.FindStringSubmatch(spec); match != nil { name := match[1] diff --git a/utils/ips_test.go b/utils/ips_test.go index 60e5bf13..48f1b656 100644 --- a/utils/ips_test.go +++ b/utils/ips_test.go @@ -125,11 +125,12 @@ func TestInterfaceIpsError(t *testing.T) { func TestInterfaceSpecParse(t *testing.T) { // Test Error Cases - testSpecError(t, "") // Nothing - testSpecError(t, "!") // Nonsense - testSpecError(t, "127.0.0.1") // No Network - testSpecError(t, "eth0:inet5") // Invalid IP Version - testSpecError(t, "eth0[-1]") // Invalid Index + testSpecError(t, "") // Nothing + testSpecError(t, "!") // Nonsense + testSpecError(t, "127.0.0.1") // No Network + testSpecError(t, "eth0:inet5") // Invalid IP Version + testSpecError(t, "eth0[-1]") // Invalid Index + testSpecError(t, "static:abcdef") // Invalid IP // Test Interface Case testSpecInterfaceName(t, "eth0", "eth0", false, -1) From 0ffe052b2485bea833c13a1cb1f12377a3697faf Mon Sep 17 00:00:00 2001 From: William Leese Date: Thu, 21 Apr 2016 14:20:40 +0200 Subject: [PATCH 8/9] add unit tests for static:x.x.x.x interface spec Change-Id: Icb2e05b43a6f4b77861bb990f183171a621e478e --- utils/ips_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/utils/ips_test.go b/utils/ips_test.go index 48f1b656..4b1a6fff 100644 --- a/utils/ips_test.go +++ b/utils/ips_test.go @@ -43,6 +43,9 @@ func TestGetIp(t *testing.T) { if ip, err := GetIP([]string{"interface-does-not-exist"}); err == nil { t.Errorf("Expected interface not found, but instead got an IP: %s", ip) } + if ip, _ := GetIP([]string{"static:192.168.1.100", "lo"}); ip != "192.168.1.100" { + t.Errorf("Expected to find static ip 192.168.1.100, but found: %s", ip) + } } func TestInterfaceIpsLoopback(t *testing.T) { @@ -139,6 +142,7 @@ func TestInterfaceSpecParse(t *testing.T) { testSpecInterfaceName(t, "eth0[2]", "eth0", false, 2) testSpecInterfaceName(t, "inet", "*", false, -1) testSpecInterfaceName(t, "inet6", "*", true, -1) + testSpecInterfaceName(t, "static:192.168.1.100", "static", false, 1) // Test CIDR Case testSpecCIDR(t, "10.0.0.0/16") @@ -156,6 +160,17 @@ func testSpecInterfaceName(t *testing.T, specStr string, name string, ipv6 bool, if err != nil { t.Errorf("Expected parse to succeed, but got error: %s", err) } + if name == "static" { + staticSpec, ok := spec.(staticInterfaceSpec) + if !ok { + t.Errorf("Expected %s to parse as staticInterfaceSpec", spec) + return + } + if staticSpec.Name != "static" { + t.Errorf("Expected to parse interface name static but got %s", staticSpec.Name) + } + return + } if index < 0 { inetSpec, ok := spec.(inetInterfaceSpec) if !ok { @@ -209,6 +224,9 @@ func TestFindIPWithSpecs(t *testing.T) { testIPSpec(t, iips, "127.0.0.1", "lo") testIPSpec(t, iips, "::1", "lo:inet6") + // Static + testIPSpec(t, iips, "192.168.1.100", "static:192.168.1.100") + // Interface Name testIPSpec(t, iips, "10.2.0.1", "eth0") testIPSpec(t, iips, "10.2.0.1", "eth0:inet") @@ -301,6 +319,7 @@ func getTestIPs() []interfaceIP { newInterfaceIP("eth2", "fdc6:238c:c4bc::1"), newInterfaceIP("lo", "::1"), newInterfaceIP("lo", "127.0.0.1"), + newInterfaceIP("static:192.168.1.100", "192.168.1.100"), } } From 11246af605927df5aa00405be5b7b2fe270e0623 Mon Sep 17 00:00:00 2001 From: William Leese Date: Fri, 22 Apr 2016 10:02:31 +0200 Subject: [PATCH 9/9] consider static:[0-9]+ alias interfaces Change-Id: I9e520ebac0516c7bfc8d65d1b7be86d8a0c3efa7 --- utils/ips.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/utils/ips.go b/utils/ips.go index 03359466..ad18441b 100644 --- a/utils/ips.go +++ b/utils/ips.go @@ -206,11 +206,13 @@ func parseInterfaceSpec(spec string) (interfaceSpec, error) { } if strings.HasPrefix(spec, "static:") { ip := strings.SplitAfter(spec, "static:") - nip := net.ParseIP(ip[1]) - if nip == nil { - return nil, fmt.Errorf("Unable to parse static ip %s in %s", ip[0], spec) + if _, err := strconv.Atoi(ip[1]); err != nil { + nip := net.ParseIP(ip[1]) + if nip == nil { + return nil, fmt.Errorf("Unable to parse static ip %s in %s", ip[0], spec) + } + return staticInterfaceSpec{Spec: spec, Name: "static", IP: nip}, nil } - return staticInterfaceSpec{Spec: spec, Name: "static", IP: nip}, nil } if match := ifaceSpec.FindStringSubmatch(spec); match != nil {