Add pkg/discovery
for nodes discovery
Absorb Swarm's discovery package in order to provide a common node discovery mechanism to be used by both Swarm and networking code. Signed-off-by: Arnaud Porterie <arnaud.porterie@docker.com>
This commit is contained in:
parent
fdd5ab2fc3
commit
35c086fa6b
13 changed files with 1015 additions and 0 deletions
41
discovery/README.md
Normal file
41
discovery/README.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
page_title: Docker discovery
|
||||||
|
page_description: discovery
|
||||||
|
page_keywords: docker, clustering, discovery
|
||||||
|
---
|
||||||
|
|
||||||
|
# Discovery
|
||||||
|
|
||||||
|
Docker comes with multiple Discovery backends.
|
||||||
|
|
||||||
|
## Backends
|
||||||
|
|
||||||
|
### Using etcd
|
||||||
|
|
||||||
|
Point your Docker Engine instances to a common etcd instance. You can specify
|
||||||
|
the address Docker uses to advertise the node using the `--discovery-address`
|
||||||
|
flag.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker daemon -H=<node_ip:2376> --discovery-address=<node_ip:2376> --discovery-backend etcd://<etcd_ip>/<path>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using consul
|
||||||
|
|
||||||
|
Point your Docker Engine instances to a common Consul instance. You can specify
|
||||||
|
the address Docker uses to advertise the node using the `--discovery-address`
|
||||||
|
flag.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker daemon -H=<node_ip:2376> --discovery-address=<node_ip:2376> --discovery-backend consul://<consul_ip>/<path>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using zookeeper
|
||||||
|
|
||||||
|
Point your Docker Engine instances to a common Zookeeper instance. You can specify
|
||||||
|
the address Docker uses to advertise the node using the `--discovery-address`
|
||||||
|
flag.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker daemon -H=<node_ip:2376> --discovery-address=<node_ip:2376> --discovery-backend zk://<zk_addr1>,<zk_addr2>>/<path>
|
||||||
|
```
|
53
discovery/backends.go
Normal file
53
discovery/backends.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Backends is a global map of discovery backends indexed by their
|
||||||
|
// associated scheme.
|
||||||
|
backends map[string]Backend
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
backends = make(map[string]Backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register makes a discovery backend available by the provided scheme.
|
||||||
|
// If Register is called twice with the same scheme an error is returned.
|
||||||
|
func Register(scheme string, d Backend) error {
|
||||||
|
if _, exists := backends[scheme]; exists {
|
||||||
|
return fmt.Errorf("scheme already registered %s", scheme)
|
||||||
|
}
|
||||||
|
log.WithField("name", scheme).Debug("Registering discovery service")
|
||||||
|
backends[scheme] = d
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(rawurl string) (string, string) {
|
||||||
|
parts := strings.SplitN(rawurl, "://", 2)
|
||||||
|
|
||||||
|
// nodes:port,node2:port => nodes://node1:port,node2:port
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return "nodes", parts[0]
|
||||||
|
}
|
||||||
|
return parts[0], parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Discovery given a URL, heartbeat and ttl settings.
|
||||||
|
// Returns an error if the URL scheme is not supported.
|
||||||
|
func New(rawurl string, heartbeat time.Duration, ttl time.Duration) (Backend, error) {
|
||||||
|
scheme, uri := parse(rawurl)
|
||||||
|
if backend, exists := backends[scheme]; exists {
|
||||||
|
log.WithFields(log.Fields{"name": scheme, "uri": uri}).Debug("Initializing discovery service")
|
||||||
|
err := backend.Initialize(uri, heartbeat, ttl)
|
||||||
|
return backend, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
35
discovery/discovery.go
Normal file
35
discovery/discovery.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNotSupported is returned when a discovery service is not supported.
|
||||||
|
ErrNotSupported = errors.New("discovery service not supported")
|
||||||
|
|
||||||
|
// ErrNotImplemented is returned when discovery feature is not implemented
|
||||||
|
// by discovery backend.
|
||||||
|
ErrNotImplemented = errors.New("not implemented in this discovery service")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher provides watching over a cluster for nodes joining and leaving.
|
||||||
|
type Watcher interface {
|
||||||
|
// Watch the discovery for entry changes.
|
||||||
|
// Returns a channel that will receive changes or an error.
|
||||||
|
// Providing a non-nil stopCh can be used to stop watching.
|
||||||
|
Watch(stopCh <-chan struct{}) (<-chan Entries, <-chan error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend is implemented by discovery backends which manage cluster entries.
|
||||||
|
type Backend interface {
|
||||||
|
// Watcher must be provided by every backend.
|
||||||
|
Watcher
|
||||||
|
|
||||||
|
// Initialize the discovery with URIs, a heartbeat and a ttl.
|
||||||
|
Initialize(string, time.Duration, time.Duration) error
|
||||||
|
|
||||||
|
// Register to the discovery.
|
||||||
|
Register(string) error
|
||||||
|
}
|
120
discovery/discovery_test.go
Normal file
120
discovery/discovery_test.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewEntry(t *testing.T) {
|
||||||
|
entry, err := NewEntry("127.0.0.1:2375")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, entry.Equals(&Entry{Host: "127.0.0.1", Port: "2375"}))
|
||||||
|
assert.Equal(t, entry.String(), "127.0.0.1:2375")
|
||||||
|
|
||||||
|
_, err = NewEntry("127.0.0.1")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
scheme, uri := parse("127.0.0.1:2375")
|
||||||
|
assert.Equal(t, scheme, "nodes")
|
||||||
|
assert.Equal(t, uri, "127.0.0.1:2375")
|
||||||
|
|
||||||
|
scheme, uri = parse("localhost:2375")
|
||||||
|
assert.Equal(t, scheme, "nodes")
|
||||||
|
assert.Equal(t, uri, "localhost:2375")
|
||||||
|
|
||||||
|
scheme, uri = parse("scheme://127.0.0.1:2375")
|
||||||
|
assert.Equal(t, scheme, "scheme")
|
||||||
|
assert.Equal(t, uri, "127.0.0.1:2375")
|
||||||
|
|
||||||
|
scheme, uri = parse("scheme://localhost:2375")
|
||||||
|
assert.Equal(t, scheme, "scheme")
|
||||||
|
assert.Equal(t, uri, "localhost:2375")
|
||||||
|
|
||||||
|
scheme, uri = parse("")
|
||||||
|
assert.Equal(t, scheme, "nodes")
|
||||||
|
assert.Equal(t, uri, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateEntries(t *testing.T) {
|
||||||
|
entries, err := CreateEntries(nil)
|
||||||
|
assert.Equal(t, entries, Entries{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
entries, err = CreateEntries([]string{"127.0.0.1:2375", "127.0.0.2:2375", ""})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
expected := Entries{
|
||||||
|
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||||
|
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||||
|
}
|
||||||
|
assert.True(t, entries.Equals(expected))
|
||||||
|
|
||||||
|
_, err = CreateEntries([]string{"127.0.0.1", "127.0.0.2"})
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsEntry(t *testing.T) {
|
||||||
|
entries, err := CreateEntries([]string{"127.0.0.1:2375", "127.0.0.2:2375", ""})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, entries.Contains(&Entry{Host: "127.0.0.1", Port: "2375"}))
|
||||||
|
assert.False(t, entries.Contains(&Entry{Host: "127.0.0.3", Port: "2375"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntriesEquality(t *testing.T) {
|
||||||
|
entries := Entries{
|
||||||
|
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||||
|
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same
|
||||||
|
assert.True(t, entries.Equals(Entries{
|
||||||
|
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||||
|
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Different size
|
||||||
|
assert.False(t, entries.Equals(Entries{
|
||||||
|
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||||
|
&Entry{Host: "127.0.0.2", Port: "2375"},
|
||||||
|
&Entry{Host: "127.0.0.3", Port: "2375"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Different content
|
||||||
|
assert.False(t, entries.Equals(Entries{
|
||||||
|
&Entry{Host: "127.0.0.1", Port: "2375"},
|
||||||
|
&Entry{Host: "127.0.0.42", Port: "2375"},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntriesDiff(t *testing.T) {
|
||||||
|
entry1 := &Entry{Host: "1.1.1.1", Port: "1111"}
|
||||||
|
entry2 := &Entry{Host: "2.2.2.2", Port: "2222"}
|
||||||
|
entry3 := &Entry{Host: "3.3.3.3", Port: "3333"}
|
||||||
|
entries := Entries{entry1, entry2}
|
||||||
|
|
||||||
|
// No diff
|
||||||
|
added, removed := entries.Diff(Entries{entry2, entry1})
|
||||||
|
assert.Empty(t, added)
|
||||||
|
assert.Empty(t, removed)
|
||||||
|
|
||||||
|
// Add
|
||||||
|
added, removed = entries.Diff(Entries{entry2, entry3, entry1})
|
||||||
|
assert.Len(t, added, 1)
|
||||||
|
assert.True(t, added.Contains(entry3))
|
||||||
|
assert.Empty(t, removed)
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
added, removed = entries.Diff(Entries{entry2})
|
||||||
|
assert.Empty(t, added)
|
||||||
|
assert.Len(t, removed, 1)
|
||||||
|
assert.True(t, removed.Contains(entry1))
|
||||||
|
|
||||||
|
// Add and remove
|
||||||
|
added, removed = entries.Diff(Entries{entry1, entry3})
|
||||||
|
assert.Len(t, added, 1)
|
||||||
|
assert.True(t, added.Contains(entry3))
|
||||||
|
assert.Len(t, removed, 1)
|
||||||
|
assert.True(t, removed.Contains(entry2))
|
||||||
|
}
|
97
discovery/entry.go
Normal file
97
discovery/entry.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEntry creates a new entry.
|
||||||
|
func NewEntry(url string) (*Entry, error) {
|
||||||
|
host, port, err := net.SplitHostPort(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Entry{host, port}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Entry represents a host.
|
||||||
|
type Entry struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns true if cmp contains the same data.
|
||||||
|
func (e *Entry) Equals(cmp *Entry) bool {
|
||||||
|
return e.Host == cmp.Host && e.Port == cmp.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string form of an entry.
|
||||||
|
func (e *Entry) String() string {
|
||||||
|
return fmt.Sprintf("%s:%s", e.Host, e.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entries is a list of *Entry with some helpers.
|
||||||
|
type Entries []*Entry
|
||||||
|
|
||||||
|
// Equals returns true if cmp contains the same data.
|
||||||
|
func (e Entries) Equals(cmp Entries) bool {
|
||||||
|
// Check if the file has really changed.
|
||||||
|
if len(e) != len(cmp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range e {
|
||||||
|
if !e[i].Equals(cmp[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns true if the Entries contain a given Entry.
|
||||||
|
func (e Entries) Contains(entry *Entry) bool {
|
||||||
|
for _, curr := range e {
|
||||||
|
if curr.Equals(entry) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff compares two entries and returns the added and removed entries.
|
||||||
|
func (e Entries) Diff(cmp Entries) (Entries, Entries) {
|
||||||
|
added := Entries{}
|
||||||
|
for _, entry := range cmp {
|
||||||
|
if !e.Contains(entry) {
|
||||||
|
added = append(added, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removed := Entries{}
|
||||||
|
for _, entry := range e {
|
||||||
|
if !cmp.Contains(entry) {
|
||||||
|
removed = append(removed, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return added, removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEntries returns an array of entries based on the given addresses.
|
||||||
|
func CreateEntries(addrs []string) (Entries, error) {
|
||||||
|
entries := Entries{}
|
||||||
|
if addrs == nil {
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if len(addr) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry, err := NewEntry(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
109
discovery/file/file.go
Normal file
109
discovery/file/file.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Discovery is exported
|
||||||
|
type Discovery struct {
|
||||||
|
heartbeat time.Duration
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init is exported
|
||||||
|
func Init() {
|
||||||
|
discovery.Register("file", &Discovery{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize is exported
|
||||||
|
func (s *Discovery) Initialize(path string, heartbeat time.Duration, ttl time.Duration) error {
|
||||||
|
s.path = path
|
||||||
|
s.heartbeat = heartbeat
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFileContent(content []byte) []string {
|
||||||
|
var result []string
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(string(content)), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
// Ignoring line starts with #
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Inlined # comment also ignored.
|
||||||
|
if strings.Contains(line, "#") {
|
||||||
|
line = line[0:strings.Index(line, "#")]
|
||||||
|
// Trim additional spaces caused by above stripping.
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
}
|
||||||
|
for _, ip := range discovery.Generate(line) {
|
||||||
|
result = append(result, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Discovery) fetch() (discovery.Entries, error) {
|
||||||
|
fileContent, err := ioutil.ReadFile(s.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read '%s': %v", s.path, err)
|
||||||
|
}
|
||||||
|
return discovery.CreateEntries(parseFileContent(fileContent))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is exported
|
||||||
|
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||||
|
ch := make(chan discovery.Entries)
|
||||||
|
errCh := make(chan error)
|
||||||
|
ticker := time.NewTicker(s.heartbeat)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(errCh)
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
// Send the initial entries if available.
|
||||||
|
currentEntries, err := s.fetch()
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
} else {
|
||||||
|
ch <- currentEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically send updates.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
newEntries, err := s.fetch()
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file has really changed.
|
||||||
|
if !newEntries.Equals(currentEntries) {
|
||||||
|
ch <- newEntries
|
||||||
|
}
|
||||||
|
currentEntries = newEntries
|
||||||
|
case <-stopCh:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register is exported
|
||||||
|
func (s *Discovery) Register(addr string) error {
|
||||||
|
return discovery.ErrNotImplemented
|
||||||
|
}
|
106
discovery/file/file_test.go
Normal file
106
discovery/file/file_test.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/discovery"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialize(t *testing.T) {
|
||||||
|
d := &Discovery{}
|
||||||
|
d.Initialize("/path/to/file", 1000, 0)
|
||||||
|
assert.Equal(t, d.path, "/path/to/file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
d, err := discovery.New("file:///path/to/file", 0, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, d.(*Discovery).path, "/path/to/file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContent(t *testing.T) {
|
||||||
|
data := `
|
||||||
|
1.1.1.[1:2]:1111
|
||||||
|
2.2.2.[2:4]:2222
|
||||||
|
`
|
||||||
|
ips := parseFileContent([]byte(data))
|
||||||
|
assert.Len(t, ips, 5)
|
||||||
|
assert.Equal(t, ips[0], "1.1.1.1:1111")
|
||||||
|
assert.Equal(t, ips[1], "1.1.1.2:1111")
|
||||||
|
assert.Equal(t, ips[2], "2.2.2.2:2222")
|
||||||
|
assert.Equal(t, ips[3], "2.2.2.3:2222")
|
||||||
|
assert.Equal(t, ips[4], "2.2.2.4:2222")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister(t *testing.T) {
|
||||||
|
discovery := &Discovery{path: "/path/to/file"}
|
||||||
|
assert.Error(t, discovery.Register("0.0.0.0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsingContentsWithComments(t *testing.T) {
|
||||||
|
data := `
|
||||||
|
### test ###
|
||||||
|
1.1.1.1:1111 # inline comment
|
||||||
|
# 2.2.2.2:2222
|
||||||
|
### empty line with comment
|
||||||
|
3.3.3.3:3333
|
||||||
|
### test ###
|
||||||
|
`
|
||||||
|
ips := parseFileContent([]byte(data))
|
||||||
|
assert.Len(t, ips, 2)
|
||||||
|
assert.Equal(t, "1.1.1.1:1111", ips[0])
|
||||||
|
assert.Equal(t, "3.3.3.3:3333", ips[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
data := `
|
||||||
|
1.1.1.1:1111
|
||||||
|
2.2.2.2:2222
|
||||||
|
`
|
||||||
|
expected := discovery.Entries{
|
||||||
|
&discovery.Entry{Host: "1.1.1.1", Port: "1111"},
|
||||||
|
&discovery.Entry{Host: "2.2.2.2", Port: "2222"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary file and remove it.
|
||||||
|
tmp, err := ioutil.TempFile(os.TempDir(), "discovery-file-test")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, tmp.Close())
|
||||||
|
assert.NoError(t, os.Remove(tmp.Name()))
|
||||||
|
|
||||||
|
// Set up file discovery.
|
||||||
|
d := &Discovery{}
|
||||||
|
d.Initialize(tmp.Name(), 1000, 0)
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
ch, errCh := d.Watch(stopCh)
|
||||||
|
|
||||||
|
// Make sure it fires errors since the file doesn't exist.
|
||||||
|
assert.Error(t, <-errCh)
|
||||||
|
// We have to drain the error channel otherwise Watch will get stuck.
|
||||||
|
go func() {
|
||||||
|
for range errCh {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Write the file and make sure we get the expected value back.
|
||||||
|
assert.NoError(t, ioutil.WriteFile(tmp.Name(), []byte(data), 0600))
|
||||||
|
assert.Equal(t, expected, <-ch)
|
||||||
|
|
||||||
|
// Add a new entry and look it up.
|
||||||
|
expected = append(expected, &discovery.Entry{Host: "3.3.3.3", Port: "3333"})
|
||||||
|
f, err := os.OpenFile(tmp.Name(), os.O_APPEND|os.O_WRONLY, 0600)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
_, err = f.WriteString("\n3.3.3.3:3333\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
f.Close()
|
||||||
|
assert.Equal(t, expected, <-ch)
|
||||||
|
|
||||||
|
// Stop and make sure it closes all channels.
|
||||||
|
close(stopCh)
|
||||||
|
assert.Nil(t, <-ch)
|
||||||
|
assert.Nil(t, <-errCh)
|
||||||
|
}
|
35
discovery/generator.go
Normal file
35
discovery/generator.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate takes care of IP generation
|
||||||
|
func Generate(pattern string) []string {
|
||||||
|
re, _ := regexp.Compile(`\[(.+):(.+)\]`)
|
||||||
|
submatch := re.FindStringSubmatch(pattern)
|
||||||
|
if submatch == nil {
|
||||||
|
return []string{pattern}
|
||||||
|
}
|
||||||
|
|
||||||
|
from, err := strconv.Atoi(submatch[1])
|
||||||
|
if err != nil {
|
||||||
|
return []string{pattern}
|
||||||
|
}
|
||||||
|
to, err := strconv.Atoi(submatch[2])
|
||||||
|
if err != nil {
|
||||||
|
return []string{pattern}
|
||||||
|
}
|
||||||
|
|
||||||
|
template := re.ReplaceAllString(pattern, "%d")
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for val := from; val <= to; val++ {
|
||||||
|
entry := fmt.Sprintf(template, val)
|
||||||
|
result = append(result, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
55
discovery/generator_test.go
Normal file
55
discovery/generator_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeneratorNotGenerate(t *testing.T) {
|
||||||
|
ips := Generate("127.0.0.1")
|
||||||
|
assert.Equal(t, len(ips), 1)
|
||||||
|
assert.Equal(t, ips[0], "127.0.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratorWithPortNotGenerate(t *testing.T) {
|
||||||
|
ips := Generate("127.0.0.1:8080")
|
||||||
|
assert.Equal(t, len(ips), 1)
|
||||||
|
assert.Equal(t, ips[0], "127.0.0.1:8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratorMatchFailedNotGenerate(t *testing.T) {
|
||||||
|
ips := Generate("127.0.0.[1]")
|
||||||
|
assert.Equal(t, len(ips), 1)
|
||||||
|
assert.Equal(t, ips[0], "127.0.0.[1]")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratorWithPort(t *testing.T) {
|
||||||
|
ips := Generate("127.0.0.[1:11]:2375")
|
||||||
|
assert.Equal(t, len(ips), 11)
|
||||||
|
assert.Equal(t, ips[0], "127.0.0.1:2375")
|
||||||
|
assert.Equal(t, ips[1], "127.0.0.2:2375")
|
||||||
|
assert.Equal(t, ips[2], "127.0.0.3:2375")
|
||||||
|
assert.Equal(t, ips[3], "127.0.0.4:2375")
|
||||||
|
assert.Equal(t, ips[4], "127.0.0.5:2375")
|
||||||
|
assert.Equal(t, ips[5], "127.0.0.6:2375")
|
||||||
|
assert.Equal(t, ips[6], "127.0.0.7:2375")
|
||||||
|
assert.Equal(t, ips[7], "127.0.0.8:2375")
|
||||||
|
assert.Equal(t, ips[8], "127.0.0.9:2375")
|
||||||
|
assert.Equal(t, ips[9], "127.0.0.10:2375")
|
||||||
|
assert.Equal(t, ips[10], "127.0.0.11:2375")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateWithMalformedInputAtRangeStart(t *testing.T) {
|
||||||
|
malformedInput := "127.0.0.[x:11]:2375"
|
||||||
|
ips := Generate(malformedInput)
|
||||||
|
assert.Equal(t, len(ips), 1)
|
||||||
|
assert.Equal(t, ips[0], malformedInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateWithMalformedInputAtRangeEnd(t *testing.T) {
|
||||||
|
malformedInput := "127.0.0.[1:x]:2375"
|
||||||
|
ips := Generate(malformedInput)
|
||||||
|
assert.Equal(t, len(ips), 1)
|
||||||
|
assert.Equal(t, ips[0], malformedInput)
|
||||||
|
}
|
148
discovery/kv/kv.go
Normal file
148
discovery/kv/kv.go
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/pkg/discovery"
|
||||||
|
"github.com/docker/libkv"
|
||||||
|
"github.com/docker/libkv/store"
|
||||||
|
"github.com/docker/libkv/store/consul"
|
||||||
|
"github.com/docker/libkv/store/etcd"
|
||||||
|
"github.com/docker/libkv/store/zookeeper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
discoveryPath = "docker/nodes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Discovery is exported
|
||||||
|
type Discovery struct {
|
||||||
|
backend store.Backend
|
||||||
|
store store.Store
|
||||||
|
heartbeat time.Duration
|
||||||
|
ttl time.Duration
|
||||||
|
prefix string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init is exported
|
||||||
|
func Init() {
|
||||||
|
// Register to libkv
|
||||||
|
zookeeper.Register()
|
||||||
|
consul.Register()
|
||||||
|
etcd.Register()
|
||||||
|
|
||||||
|
// Register to internal discovery service
|
||||||
|
discovery.Register("zk", &Discovery{backend: store.ZK})
|
||||||
|
discovery.Register("consul", &Discovery{backend: store.CONSUL})
|
||||||
|
discovery.Register("etcd", &Discovery{backend: store.ETCD})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize is exported
|
||||||
|
func (s *Discovery) Initialize(uris string, heartbeat time.Duration, ttl time.Duration) error {
|
||||||
|
var (
|
||||||
|
parts = strings.SplitN(uris, "/", 2)
|
||||||
|
addrs = strings.Split(parts[0], ",")
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// A custom prefix to the path can be optionally used.
|
||||||
|
if len(parts) == 2 {
|
||||||
|
s.prefix = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
s.heartbeat = heartbeat
|
||||||
|
s.ttl = ttl
|
||||||
|
s.path = path.Join(s.prefix, discoveryPath)
|
||||||
|
|
||||||
|
// Creates a new store, will ignore options given
|
||||||
|
// if not supported by the chosen store
|
||||||
|
s.store, err = libkv.NewStore(s.backend, addrs, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch the store until either there's a store error or we receive a stop request.
|
||||||
|
// Returns false if we shouldn't attempt watching the store anymore (stop request received).
|
||||||
|
func (s *Discovery) watchOnce(stopCh <-chan struct{}, watchCh <-chan []*store.KVPair, discoveryCh chan discovery.Entries, errCh chan error) bool {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case pairs := <-watchCh:
|
||||||
|
if pairs == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("discovery", s.backend).Debugf("Watch triggered with %d nodes", len(pairs))
|
||||||
|
|
||||||
|
// Convert `KVPair` into `discovery.Entry`.
|
||||||
|
addrs := make([]string, len(pairs))
|
||||||
|
for _, pair := range pairs {
|
||||||
|
addrs = append(addrs, string(pair.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := discovery.CreateEntries(addrs)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
} else {
|
||||||
|
discoveryCh <- entries
|
||||||
|
}
|
||||||
|
case <-stopCh:
|
||||||
|
// We were requested to stop watching.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is exported
|
||||||
|
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||||
|
ch := make(chan discovery.Entries)
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer close(errCh)
|
||||||
|
|
||||||
|
// Forever: Create a store watch, watch until we get an error and then try again.
|
||||||
|
// Will only stop if we receive a stopCh request.
|
||||||
|
for {
|
||||||
|
// Set up a watch.
|
||||||
|
watchCh, err := s.store.WatchTree(s.path, stopCh)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
} else {
|
||||||
|
if !s.watchOnce(stopCh, watchCh, ch, errCh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here it means the store watch channel was closed. This
|
||||||
|
// is unexpected so let's retry later.
|
||||||
|
errCh <- fmt.Errorf("Unexpected watch error")
|
||||||
|
time.Sleep(s.heartbeat)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ch, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register is exported
|
||||||
|
func (s *Discovery) Register(addr string) error {
|
||||||
|
opts := &store.WriteOptions{TTL: s.ttl}
|
||||||
|
return s.store.Put(path.Join(s.path, addr), []byte(addr), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store returns the underlying store used by KV discovery.
|
||||||
|
func (s *Discovery) Store() store.Store {
|
||||||
|
return s.store
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix returns the store prefix
|
||||||
|
func (s *Discovery) Prefix() string {
|
||||||
|
return s.prefix
|
||||||
|
}
|
119
discovery/kv/kv_test.go
Normal file
119
discovery/kv/kv_test.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package kv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/discovery"
|
||||||
|
"github.com/docker/libkv/store"
|
||||||
|
libkvmock "github.com/docker/libkv/store/mock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialize(t *testing.T) {
|
||||||
|
storeMock, err := libkvmock.New([]string{"127.0.0.1"}, nil)
|
||||||
|
assert.NotNil(t, storeMock)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
d := &Discovery{backend: store.CONSUL}
|
||||||
|
d.Initialize("127.0.0.1", 0, 0)
|
||||||
|
d.store = storeMock
|
||||||
|
|
||||||
|
s := d.store.(*libkvmock.Mock)
|
||||||
|
assert.Len(t, s.Endpoints, 1)
|
||||||
|
assert.Equal(t, s.Endpoints[0], "127.0.0.1")
|
||||||
|
assert.Equal(t, d.path, discoveryPath)
|
||||||
|
|
||||||
|
storeMock, err = libkvmock.New([]string{"127.0.0.1:1234"}, nil)
|
||||||
|
assert.NotNil(t, storeMock)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
d = &Discovery{backend: store.CONSUL}
|
||||||
|
d.Initialize("127.0.0.1:1234/path", 0, 0)
|
||||||
|
d.store = storeMock
|
||||||
|
|
||||||
|
s = d.store.(*libkvmock.Mock)
|
||||||
|
assert.Len(t, s.Endpoints, 1)
|
||||||
|
assert.Equal(t, s.Endpoints[0], "127.0.0.1:1234")
|
||||||
|
assert.Equal(t, d.path, "path/"+discoveryPath)
|
||||||
|
|
||||||
|
storeMock, err = libkvmock.New([]string{"127.0.0.1:1234", "127.0.0.2:1234", "127.0.0.3:1234"}, nil)
|
||||||
|
assert.NotNil(t, storeMock)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
d = &Discovery{backend: store.CONSUL}
|
||||||
|
d.Initialize("127.0.0.1:1234,127.0.0.2:1234,127.0.0.3:1234/path", 0, 0)
|
||||||
|
d.store = storeMock
|
||||||
|
|
||||||
|
s = d.store.(*libkvmock.Mock)
|
||||||
|
if assert.Len(t, s.Endpoints, 3) {
|
||||||
|
assert.Equal(t, s.Endpoints[0], "127.0.0.1:1234")
|
||||||
|
assert.Equal(t, s.Endpoints[1], "127.0.0.2:1234")
|
||||||
|
assert.Equal(t, s.Endpoints[2], "127.0.0.3:1234")
|
||||||
|
}
|
||||||
|
assert.Equal(t, d.path, "path/"+discoveryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
storeMock, err := libkvmock.New([]string{"127.0.0.1:1234"}, nil)
|
||||||
|
assert.NotNil(t, storeMock)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
d := &Discovery{backend: store.CONSUL}
|
||||||
|
d.Initialize("127.0.0.1:1234/path", 0, 0)
|
||||||
|
d.store = storeMock
|
||||||
|
|
||||||
|
s := d.store.(*libkvmock.Mock)
|
||||||
|
mockCh := make(chan []*store.KVPair)
|
||||||
|
|
||||||
|
// The first watch will fail.
|
||||||
|
s.On("WatchTree", "path/"+discoveryPath, mock.Anything).Return(mockCh, errors.New("test error")).Once()
|
||||||
|
// The second one will succeed.
|
||||||
|
s.On("WatchTree", "path/"+discoveryPath, mock.Anything).Return(mockCh, nil).Once()
|
||||||
|
expected := discovery.Entries{
|
||||||
|
&discovery.Entry{Host: "1.1.1.1", Port: "1111"},
|
||||||
|
&discovery.Entry{Host: "2.2.2.2", Port: "2222"},
|
||||||
|
}
|
||||||
|
kvs := []*store.KVPair{
|
||||||
|
{Key: path.Join("path", discoveryPath, "1.1.1.1"), Value: []byte("1.1.1.1:1111")},
|
||||||
|
{Key: path.Join("path", discoveryPath, "2.2.2.2"), Value: []byte("2.2.2.2:2222")},
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
ch, errCh := d.Watch(stopCh)
|
||||||
|
|
||||||
|
// It should fire an error since the first WatchTree call failed.
|
||||||
|
assert.EqualError(t, <-errCh, "test error")
|
||||||
|
// We have to drain the error channel otherwise Watch will get stuck.
|
||||||
|
go func() {
|
||||||
|
for range errCh {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Push the entries into the store channel and make sure discovery emits.
|
||||||
|
mockCh <- kvs
|
||||||
|
assert.Equal(t, <-ch, expected)
|
||||||
|
|
||||||
|
// Add a new entry.
|
||||||
|
expected = append(expected, &discovery.Entry{Host: "3.3.3.3", Port: "3333"})
|
||||||
|
kvs = append(kvs, &store.KVPair{Key: path.Join("path", discoveryPath, "3.3.3.3"), Value: []byte("3.3.3.3:3333")})
|
||||||
|
mockCh <- kvs
|
||||||
|
assert.Equal(t, <-ch, expected)
|
||||||
|
|
||||||
|
// Make sure that if an error occurs it retries.
|
||||||
|
// This third call to WatchTree will be checked later by AssertExpectations.
|
||||||
|
s.On("WatchTree", "path/"+discoveryPath, mock.Anything).Return(mockCh, nil)
|
||||||
|
close(mockCh)
|
||||||
|
// Give it enough time to call WatchTree.
|
||||||
|
time.Sleep(3)
|
||||||
|
|
||||||
|
// Stop and make sure it closes all channels.
|
||||||
|
close(stopCh)
|
||||||
|
assert.Nil(t, <-ch)
|
||||||
|
assert.Nil(t, <-errCh)
|
||||||
|
|
||||||
|
s.AssertExpectations(t)
|
||||||
|
}
|
54
discovery/nodes/nodes.go
Normal file
54
discovery/nodes/nodes.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package nodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Discovery is exported
|
||||||
|
type Discovery struct {
|
||||||
|
entries discovery.Entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init is exported
|
||||||
|
func Init() {
|
||||||
|
discovery.Register("nodes", &Discovery{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize is exported
|
||||||
|
func (s *Discovery) Initialize(uris string, _ time.Duration, _ time.Duration) error {
|
||||||
|
for _, input := range strings.Split(uris, ",") {
|
||||||
|
for _, ip := range discovery.Generate(input) {
|
||||||
|
entry, err := discovery.NewEntry(ip)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s, please check you are using the correct discovery (missing token:// ?)", err.Error())
|
||||||
|
}
|
||||||
|
s.entries = append(s.entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is exported
|
||||||
|
func (s *Discovery) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
|
||||||
|
ch := make(chan discovery.Entries)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
ch <- s.entries
|
||||||
|
<-stopCh
|
||||||
|
}()
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register is exported
|
||||||
|
func (s *Discovery) Register(addr string) error {
|
||||||
|
return discovery.ErrNotImplemented
|
||||||
|
}
|
43
discovery/nodes/nodes_test.go
Normal file
43
discovery/nodes/nodes_test.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package nodes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/discovery"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialize(t *testing.T) {
|
||||||
|
d := &Discovery{}
|
||||||
|
d.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0, 0)
|
||||||
|
assert.Equal(t, len(d.entries), 2)
|
||||||
|
assert.Equal(t, d.entries[0].String(), "1.1.1.1:1111")
|
||||||
|
assert.Equal(t, d.entries[1].String(), "2.2.2.2:2222")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitializeWithPattern(t *testing.T) {
|
||||||
|
d := &Discovery{}
|
||||||
|
d.Initialize("1.1.1.[1:2]:1111,2.2.2.[2:4]:2222", 0, 0)
|
||||||
|
assert.Equal(t, len(d.entries), 5)
|
||||||
|
assert.Equal(t, d.entries[0].String(), "1.1.1.1:1111")
|
||||||
|
assert.Equal(t, d.entries[1].String(), "1.1.1.2:1111")
|
||||||
|
assert.Equal(t, d.entries[2].String(), "2.2.2.2:2222")
|
||||||
|
assert.Equal(t, d.entries[3].String(), "2.2.2.3:2222")
|
||||||
|
assert.Equal(t, d.entries[4].String(), "2.2.2.4:2222")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatch(t *testing.T) {
|
||||||
|
d := &Discovery{}
|
||||||
|
d.Initialize("1.1.1.1:1111,2.2.2.2:2222", 0, 0)
|
||||||
|
expected := discovery.Entries{
|
||||||
|
&discovery.Entry{Host: "1.1.1.1", Port: "1111"},
|
||||||
|
&discovery.Entry{Host: "2.2.2.2", Port: "2222"},
|
||||||
|
}
|
||||||
|
ch, _ := d.Watch(nil)
|
||||||
|
assert.True(t, expected.Equals(<-ch))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegister(t *testing.T) {
|
||||||
|
d := &Discovery{}
|
||||||
|
assert.Error(t, d.Register("0.0.0.0"))
|
||||||
|
}
|
Loading…
Reference in a new issue