Commit 9a26e4bd authored by ale's avatar ale

export etcd cluster info via DNS

This change involves a refactoring of the DNS redirector code, which is now a bit cleaner and has proper tests.
parent f0b6769f
......@@ -264,7 +264,7 @@ func (ns *NodeStatus) NumListeners() int {
// Client is the actual API to the streaming cluster's database.
type Client struct {
client EtcdClient
EtcdClient EtcdClient
presenceCache *presence.Cache
}
......@@ -274,7 +274,7 @@ func NewClient(client EtcdClient) *Client {
// since it is likely that it will be accessed quite often (in
// the case of redirectord, on every request).
return &Client{
client: client,
EtcdClient: client,
presenceCache: presence.NewCache(client, NodePrefix, 2*time.Second, func(data []string) interface{} {
// Convert a list of JSON-encoded NodeStatus
// objects into a lisce of *NodeStatus
......@@ -293,6 +293,11 @@ func NewClient(client EtcdClient) *Client {
}
}
// WaitForNodes waits until the node presence cache is initialized.
func (r *Client) WaitForNodes() {
r.presenceCache.WaitForInit()
}
// GetNodes returns the list of active cluster nodes.
func (r *Client) GetNodes() ([]*NodeStatus, error) {
data, err := r.presenceCache.Data()
......@@ -305,7 +310,7 @@ func (r *Client) GetNodes() ([]*NodeStatus, error) {
// GetMount returns data on a specific mountpoint (returns nil if not
// found).
func (r *Client) GetMount(mountName string) (*Mount, error) {
response, err := r.client.Get(mountEtcdPath(mountName), false, false)
response, err := r.EtcdClient.Get(mountEtcdPath(mountName), false, false)
if err != nil || response.Node == nil {
return nil, err
}
......@@ -327,19 +332,19 @@ func (r *Client) SetMount(m *Mount) error {
return err
}
_, err := r.client.Set(mountEtcdPath(m.Name), buf.String(), 0)
_, err := r.EtcdClient.Set(mountEtcdPath(m.Name), buf.String(), 0)
return err
}
// DelMount removes a mountpoint.
func (r *Client) DelMount(mountName string) error {
_, err := r.client.Delete(mountEtcdPath(mountName), false)
_, err := r.EtcdClient.Delete(mountEtcdPath(mountName), false)
return err
}
// ListMounts returns a list of all the configured mountpoints.
func (r *Client) ListMounts() ([]*Mount, error) {
response, err := r.client.Get(MountPrefix, true, false)
response, err := r.EtcdClient.Get(MountPrefix, true, false)
if err != nil || response.Node == nil {
return nil, err
}
......@@ -384,7 +389,7 @@ func (m MasterNodeInfo) GetInternalIP() []net.IP {
// GetMasterInfo returns the address of the current master server.
func (r *Client) GetMasterInfo() (*MasterNodeInfo, error) {
response, err := r.client.Get(MasterElectionPath, false, false)
response, err := r.EtcdClient.Get(MasterElectionPath, false, false)
if err != nil || response.Node == nil {
return nil, err
}
......
......@@ -286,3 +286,11 @@ func (s *FakeEtcdClient) Watch(key string, index uint64, recursive bool, respch
}
return resp, nil
}
func (s *FakeEtcdClient) GetCluster() []string {
return []string{"http://localhost:2379"}
}
func (s *FakeEtcdClient) SyncCluster() bool {
return false
}
......@@ -74,7 +74,7 @@ func (c *Cache) run(refresh time.Duration) {
}
doUpdate()
c.loaded <- struct{}{}
close(c.loaded)
for {
select {
case <-tick.C:
......
......@@ -7,6 +7,7 @@ package presence
import (
"log"
"strings"
"time"
"git.autistici.org/ale/autoradio/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd"
......@@ -33,7 +34,7 @@ type Client struct {
func NewClient(client EtcdClient, path string) *Client {
return &Client{
client: client,
path: path,
path: strings.TrimRight(path, "/"),
}
}
......
......@@ -94,7 +94,9 @@ type EtcdClient interface {
CompareAndSwap(string, string, uint64, string, uint64) (*etcd.Response, error)
Delete(string, bool) (*etcd.Response, error)
Get(string, bool, bool) (*etcd.Response, error)
GetCluster() []string
Set(string, string, uint64) (*etcd.Response, error)
SyncCluster() bool
Update(string, string, uint64) (*etcd.Response, error)
Watch(string, uint64, bool, chan *etcd.Response, chan bool) (*etcd.Response, error)
}
This diff is collapsed.
package fe
import (
"encoding/json"
"log"
"net"
"testing"
"git.autistici.org/ale/autoradio/Godeps/_workspace/src/github.com/miekg/dns"
"git.autistici.org/ale/autoradio"
"git.autistici.org/ale/autoradio/coordination/etcdtest"
)
func createTestDNSRedirector(t testing.TB, withNode bool) *DNSRedirector {
etcd := etcdtest.NewClient()
if withNode {
// Create fake presence data, so that GetNodes() returns something.
nodeData, _ := json.Marshal(&autoradio.NodeStatus{
Name: "node1",
IcecastUp: true,
IP: []net.IP{
net.ParseIP("1.2.3.4"),
net.ParseIP("2001:a:b::1"),
},
})
log.Printf("creating %s", autoradio.NodePrefix+"0001")
if _, err := etcd.Create(autoradio.NodePrefix+"0001", string(nodeData), 86400); err != nil {
t.Fatalf("etcd.Create(): %v", err)
}
}
client := autoradio.NewClient(etcd)
client.WaitForNodes()
d := NewDNSRedirector(client, "example.com", []net.IP{net.ParseIP("2.3.4.5")}, 30)
d.updateEtcdCluster()
return d
}
type testNetAddr struct{}
func (n testNetAddr) Network() string { return "tcp" }
func (n testNetAddr) String() string { return "127.0.0.1" }
// Fake dns.ResponseWriter that records messages written to it.
type testDNSResponseWriter struct {
messages []*dns.Msg
}
// WriteMsg writes a reply back to the client.
func (w *testDNSResponseWriter) WriteMsg(m *dns.Msg) error {
w.messages = append(w.messages, m)
return nil
}
// LocalAddr returns the net.Addr of the server
func (w *testDNSResponseWriter) LocalAddr() net.Addr { return &testNetAddr{} }
// RemoteAddr returns the net.Addr of the client that sent the current request.
func (w *testDNSResponseWriter) RemoteAddr() net.Addr { return &testNetAddr{} }
// Write writes a raw buffer back to the client.
func (w *testDNSResponseWriter) Write([]byte) (int, error) { return 0, nil }
// Close closes the connection.
func (w *testDNSResponseWriter) Close() error { return nil }
// TsigStatus returns the status of the Tsig.
func (w *testDNSResponseWriter) TsigStatus() error { return nil }
// TsigTimersOnly sets the tsig timers only boolean.
func (w *testDNSResponseWriter) TsigTimersOnly(bool) {}
// Hijack lets the caller take over the connection.
// After a call to Hijack(), the DNS package will not do anything with the connection.
func (w *testDNSResponseWriter) Hijack() {}
func TestDNSRedirector_A(t *testing.T) {
testQueryA(t, true, "stream.example.com.", "1.2.3.4")
}
func TestDNSRedirector_A_Etcd(t *testing.T) {
testQueryA(t, false, "etcd.example.com.", "127.0.0.1")
}
func TestDNSRedirector_A_Fallback(t *testing.T) {
testQueryA(t, false, "stream.example.com.", "2.3.4.5")
}
func doTestQuery(t testing.TB, withNode bool, q *dns.Msg) *dns.Msg {
d := createTestDNSRedirector(t, withNode)
w := &testDNSResponseWriter{}
d.ServeDNS(w, q)
if len(w.messages) != 1 {
t.Fatal("no reply")
}
return w.messages[0]
}
func testQueryA(t testing.TB, withNode bool, query, expected string) {
q := new(dns.Msg)
q.SetQuestion(query, dns.TypeA)
response := doTestQuery(t, withNode, q)
answer, ok := response.Answer[0].(*dns.A)
if !ok {
t.Fatalf("expected A, got: %s:", response.Answer[0])
}
if answer.A.String() != expected {
t.Fatalf("bad IP: %s, expected %s", answer.A, expected)
}
}
func TestDNSRedirector_AAAA(t *testing.T) {
q := new(dns.Msg)
q.SetQuestion("stream.example.com", dns.TypeAAAA)
response := doTestQuery(t, true, q)
answer, ok := response.Answer[0].(*dns.AAAA)
if !ok {
t.Fatalf("bad reply (not AAAA): %s:", response.Answer[0])
}
expected := "2001:a:b::1"
if answer.AAAA.String() != expected {
t.Fatalf("bad IP: %s, expected %s", answer.AAAA, expected)
}
}
func TestDNSRedirector_NXDOMAIN(t *testing.T) {
q := new(dns.Msg)
q.SetQuestion("nonexisting.example.com", dns.TypeA)
response := doTestQuery(t, false, q)
if response.MsgHdr.Rcode != dns.RcodeNameError {
t.Fatalf("expected NXDOMAIN, got: %s", response)
}
}
func TestDNSRedirector_NXDOMAIN_WrongZone(t *testing.T) {
q := new(dns.Msg)
q.SetQuestion("foo.bar.com", dns.TypeA)
response := doTestQuery(t, false, q)
if response.MsgHdr.Rcode != dns.RcodeNameError {
t.Fatalf("expected NXDOMAIN, got: %s", response)
}
}
func TestDNSRedirector_SRV_Etcd(t *testing.T) {
q := new(dns.Msg)
q.SetQuestion("_etcd-server._tcp.example.com", dns.TypeSRV)
response := doTestQuery(t, false, q)
if len(response.Answer) != 1 {
t.Fatalf("expected 1 answer, got: %s", response)
}
srv, ok := response.Answer[0].(*dns.SRV)
if !ok {
t.Fatalf("expected SRV, got: %s", response)
}
if srv.Port != 2379 {
t.Fatalf("expected port 2379, got: %s", srv)
}
if srv.Target != "localhost." {
t.Fatalf("expected target localhost, got: %s", srv)
}
}
func TestDNSRedirector_SRV_Etcd_BadScheme(t *testing.T) {
q := new(dns.Msg)
q.SetQuestion("_etcd-server-ssl._tcp.example.com", dns.TypeSRV)
response := doTestQuery(t, false, q)
if response.MsgHdr.Rcode != dns.RcodeNameError {
t.Fatalf("expected NXDOMAIN, got: %s", response)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment