diff --git a/CHANGELOG.md b/CHANGELOG.md index c2954ce23..97f87722e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.2 (Unreleased) + +IMPROVEMENTS: + + * DNS case-insensitivity [GH-189] + ## 0.3.1 (July 21, 2014) FEATURES: diff --git a/command/agent/dns.go b/command/agent/dns.go index 522376e15..b83243720 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -248,7 +248,7 @@ func (d *DNSServer) dispatch(network string, req, resp *dns.Msg) { datacenter := d.agent.config.Datacenter // Get the QName without the domain suffix - qName := dns.Fqdn(req.Question[0].Name) + qName := strings.ToLower(dns.Fqdn(req.Question[0].Name)) qName = strings.TrimSuffix(qName, d.domain) // Split into the label parts diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index cf1ae791f..7d4226614 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -136,6 +136,40 @@ func TestDNS_NodeLookup(t *testing.T) { } } +func TestDNS_CaseInsensitiveNodeLookup(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register node + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "Foo", + Address: "127.0.0.1", + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + m := new(dns.Msg) + m.SetQuestion("fOO.node.dc1.consul.", dns.TypeANY) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener(srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("empty lookup: %#v", in) + } +} + func TestDNS_NodeLookup_PeriodName(t *testing.T) { dir, srv := makeDNSServer(t) defer os.RemoveAll(dir) @@ -336,6 +370,45 @@ func TestDNS_ServiceLookup(t *testing.T) { } } +func TestDNS_CaseInsensitiveServiceLookup(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register node + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "Db", + Tags: []string{"Master"}, + Port: 12345, + }, + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + m := new(dns.Msg) + m.SetQuestion("mASTER.dB.service.consul.", dns.TypeSRV) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener(srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("empty lookup: %#v", in) + } +} + func TestDNS_ServiceLookup_TagPeriod(t *testing.T) { dir, srv := makeDNSServer(t) defer os.RemoveAll(dir) diff --git a/consul/catalog_endpoint_test.go b/consul/catalog_endpoint_test.go index f9721ca09..06066b7db 100644 --- a/consul/catalog_endpoint_test.go +++ b/consul/catalog_endpoint_test.go @@ -220,13 +220,13 @@ func TestCatalogListNodes(t *testing.T) { }) // Server node is auto added from Serf - if out.Nodes[0].Node != s1.config.NodeName { + if out.Nodes[1].Node != s1.config.NodeName { t.Fatalf("bad: %v", out) } - if out.Nodes[1].Node != "foo" { + if out.Nodes[0].Node != "foo" { t.Fatalf("bad: %v", out) } - if out.Nodes[1].Address != "127.0.0.1" { + if out.Nodes[0].Address != "127.0.0.1" { t.Fatalf("bad: %v", out) } } diff --git a/consul/mdb_table.go b/consul/mdb_table.go index c4c84b0dc..592bce849 100644 --- a/consul/mdb_table.go +++ b/consul/mdb_table.go @@ -45,12 +45,13 @@ type MDBTables []*MDBTable // An Index is named, and uses a series of column values to // map to the row-id containing the table type MDBIndex struct { - AllowBlank bool // Can fields be blank - Unique bool // Controls if values are unique - Fields []string // Fields are used to build the index - IdxFunc IndexFunc // Can be used to provide custom indexing - Virtual bool // Virtual index does not exist, but can be used for queries - RealIndex string // Virtual indexes use a RealIndex for iteration + AllowBlank bool // Can fields be blank + Unique bool // Controls if values are unique + Fields []string // Fields are used to build the index + IdxFunc IndexFunc // Can be used to provide custom indexing + Virtual bool // Virtual index does not exist, but can be used for queries + RealIndex string // Virtual indexes use a RealIndex for iteration + CaseInsensitive bool // Controls if values are case-insensitive table *MDBTable name string @@ -426,6 +427,10 @@ func (t *MDBTable) getIndex(index string, parts []string) (*MDBIndex, []byte, er return nil, nil, tooManyFields } + if idx.CaseInsensitive { + parts = ToLowerList(parts) + } + // Construct the key key := idx.keyFromParts(parts...) return idx, key, nil @@ -613,6 +618,9 @@ func (i *MDBIndex) keyFromObject(obj interface{}) ([]byte, error) { if !i.AllowBlank && val == "" { return nil, fmt.Errorf("Field '%s' must be set: %#v", field, obj) } + if i.CaseInsensitive { + val = strings.ToLower(val) + } parts = append(parts, val) } key := i.keyFromParts(parts...) diff --git a/consul/state_store.go b/consul/state_store.go index a2f139af1..12e1c6c45 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -179,6 +179,7 @@ func (s *StateStore) initialize() error { "id": &MDBIndex{ Unique: true, Fields: []string{"Node"}, + CaseInsensitive: true, }, }, Decoder: func(buf []byte) interface{} { @@ -200,6 +201,7 @@ func (s *StateStore) initialize() error { "service": &MDBIndex{ AllowBlank: true, Fields: []string{"ServiceName"}, + CaseInsensitive: true, }, }, Decoder: func(buf []byte) interface{} { @@ -640,7 +642,7 @@ func serviceTagFilter(l []interface{}, tag string) []interface{} { n := len(l) for i := 0; i < n; i++ { srv := l[i].(*structs.ServiceNode) - if !strContains(srv.ServiceTags, tag) { + if !strContains(ToLowerList(srv.ServiceTags), strings.ToLower(tag)) { l[i], l[n-1] = l[n-1], nil i-- n-- diff --git a/consul/util.go b/consul/util.go index 00815ea10..96ee5c327 100644 --- a/consul/util.go +++ b/consul/util.go @@ -9,6 +9,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "github.com/hashicorp/serf/serf" ) @@ -68,6 +69,14 @@ func strContains(l []string, s string) bool { return false } +func ToLowerList(l []string) []string { + var out []string + for _, value := range l { + out = append(out, strings.ToLower(value)) + } + return out +} + // ensurePath is used to make sure a path exists func ensurePath(path string, dir bool) error { if !dir { diff --git a/consul/util_test.go b/consul/util_test.go index 107146b52..91b7fd2f5 100644 --- a/consul/util_test.go +++ b/consul/util_test.go @@ -18,6 +18,15 @@ func TestStrContains(t *testing.T) { } } +func TestToLowerList(t *testing.T) { + l := []string{"ABC", "Abc", "abc"} + for _, value := range ToLowerList(l) { + if value != "abc" { + t.Fatalf("failed lowercasing") + } + } +} + func TestIsPrivateIP(t *testing.T) { if !isPrivateIP("192.168.1.1") { t.Fatalf("bad") diff --git a/website/source/docs/agent/dns.html.markdown b/website/source/docs/agent/dns.html.markdown index 46303a60d..938c989a6 100644 --- a/website/source/docs/agent/dns.html.markdown +++ b/website/source/docs/agent/dns.html.markdown @@ -20,7 +20,8 @@ with no failing health checks. It's that simple! There are a number of [configuration options](/docs/agent/options.html) that are important for the DNS interface. They are `client_addr`, `ports.dns`, `recursor`, `domain`, and `dns_config`. By default Consul will listen on 127.0.0.1:8600 for DNS queries -in the "consul." domain, without support for DNS recursion. +in the "consul." domain, without support for DNS recursion. All queries are case-insensitive, a +name lookup for `PostgreSQL.node.dc1.consul` will find all nodes named `postgresql`, no matter of case. There are a few ways to use the DNS interface. One option is to use a custom DNS resolver library and point it at Consul. Another option is to set Consul