Skip to content

Commit

Permalink
GODRIVER-89 Implement Initial DNS Seedlist discovery spec
Browse files Browse the repository at this point in the history
  • Loading branch information
Will Banfield committed Nov 10, 2017
1 parent 79367d0 commit aec5f69
Show file tree
Hide file tree
Showing 21 changed files with 448 additions and 69 deletions.
4 changes: 4 additions & 0 deletions .evergreen/.evg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@ tasks:
tags: ["test", "replicaset"]
commands:
- func: bootstrap-mongo-orchestration
vars:
TOPOLOGY: "replica_set"
AUTH: "noauth"
SSL: "nossl"
- func: run-tests
vars:
TOPOLOGY: "replica_set"
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ update-bson-corpus-tests:
update-connection-string-tests:
etc/update-spec-tests.sh connection-string

.PHONY: update-initial-dns-seedlist-discovery-tests
update-initial-dns-seedlist-discovery-tests:
etc/update-spec-tests.sh initial-dns-seedlist-discovery

.PHONY: update-max-staleness-tests
update-max-staleness-tests:
etc/update-spec-tests.sh max-staleness
Expand Down
57 changes: 57 additions & 0 deletions data/initial-dns-seedlist-discovery/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
====================================
Initial DNS Seedlist Discovery tests
====================================

This directory contains platform-independent tests that drivers can use
to prove their conformance to the Initial DNS Seedlist Discovery spec.

Test Setup
----------

Start a three-node replica set on localhost, on ports 27017, 27018, and 27019,
with replica set name "repl0".

To run the tests that accompany this spec, you need to configure the SRV and
TXT records with a real name server. The following records are required for
these tests::

Record TTL Class Port Target
_mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc.
_mongodb._tcp.test1.test.build.10gen.cc. 86400 IN SRV 27018 localhost.build.10gen.cc.
_mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27018 localhost.build.10gen.cc.
_mongodb._tcp.test2.test.build.10gen.cc. 86400 IN SRV 27019 localhost.build.10gen.cc.
_mongodb._tcp.test3.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc.
_mongodb._tcp.test5.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc.
_mongodb._tcp.test6.test.build.10gen.cc. 86400 IN SRV 27017 localhost.build.10gen.cc.

Record TTL Class Text
test5.test.build.10gen.cc. 86400 IN TXT "connectTimeoutMS=300000&socketTimeoutMS=300000"
test6.test.build.10gen.cc. 86400 IN TXT "connectTimeoutMS=200000"
test6.test.build.10gen.cc. 86400 IN TXT "socketTimeoutMS=200000"

Note that ``test4`` is omitted deliberately to test what happens with no SRV
record.

In our tests we have used "localhost.build.10gen.cc" as the domain, and then
configured "localhost.build.10gen.cc" to resolve to 127.0.0.1.

You need to adapt the records shown above to replace ``build.10gen.cc`` with
your own domain name, and update the "uri" field in the YAML or JSON files in
this directory with the actual domain.

Test Format and Use
-------------------

These YAML and JSON files contain the following fields:

- ``uri``: a mongodb+srv connection string
- ``seeds``: the expected set of initial seeds discovered from the SRV record
- ``hosts``: the discovered topology's list of hosts once SDAM completes a scan
- ``options``: the parsed connection string options as discovered from URI and
TXT records

For each file, create MongoClient initialized with the mongodb+srv connection
string. You SHOULD verify that the client's initial seed list matches the list of
seeds. You MUST verify that the set of ServerDescriptions in the client's
TopologyDescription eventually matches the list of hosts. You MUST verify that
the set of Connection String Options matches the client's parsed set.
5 changes: 5 additions & 0 deletions data/initial-dns-seedlist-discovery/no-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"uri": "mongodb+srv://test4.test.build.10gen.cc/",
"seeds": [],
"hosts": []
}
3 changes: 3 additions & 0 deletions data/initial-dns-seedlist-discovery/no-results.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
uri: "mongodb+srv://test4.test.build.10gen.cc/"
seeds: []
hosts: []
11 changes: 11 additions & 0 deletions data/initial-dns-seedlist-discovery/one-result-default-port.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"uri": "mongodb+srv://test3.test.build.10gen.cc/?replicaSet=repl0",
"seeds": [
"localhost.build.10gen.cc:27017"
],
"hosts": [
"localhost:27017",
"localhost:27018",
"localhost:27019"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
uri: "mongodb+srv://test3.test.build.10gen.cc/?replicaSet=repl0"
seeds:
- localhost.build.10gen.cc:27017
hosts:
- localhost:27017
- localhost:27018
- localhost:27019
16 changes: 16 additions & 0 deletions data/initial-dns-seedlist-discovery/one-txt-record.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"uri": "mongodb+srv://test5.test.build.10gen.cc/?replicaSet=repl0",
"seeds": [
"localhost.build.10gen.cc:27017"
],
"hosts": [
"localhost:27017",
"localhost:27018",
"localhost:27019"
],
"options": {
"connectTimeoutMS": 300000,
"replicaSet": "repl0",
"socketTimeoutMS": 300000
}
}
11 changes: 11 additions & 0 deletions data/initial-dns-seedlist-discovery/one-txt-record.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
uri: "mongodb+srv://test5.test.build.10gen.cc/?replicaSet=repl0"
seeds:
- localhost.build.10gen.cc:27017
hosts:
- localhost:27017
- localhost:27018
- localhost:27019
options:
connectTimeoutMS: 300000
replicaSet: repl0
socketTimeoutMS: 300000
12 changes: 12 additions & 0 deletions data/initial-dns-seedlist-discovery/two-results-default-port.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"uri": "mongodb+srv://test1.test.build.10gen.cc/?replicaSet=repl0",
"seeds": [
"localhost.build.10gen.cc:27017",
"localhost.build.10gen.cc:27018"
],
"hosts": [
"localhost:27017",
"localhost:27018",
"localhost:27019"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
uri: "mongodb+srv://test1.test.build.10gen.cc/?replicaSet=repl0"
seeds:
- localhost.build.10gen.cc:27017
- localhost.build.10gen.cc:27018
hosts:
- localhost:27017
- localhost:27018
- localhost:27019
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"uri": "mongodb+srv://test2.test.build.10gen.cc/?replicaSet=repl0",
"seeds": [
"localhost.build.10gen.cc:27018",
"localhost.build.10gen.cc:27019"
],
"hosts": [
"localhost:27017",
"localhost:27018",
"localhost:27019"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
uri: "mongodb+srv://test2.test.build.10gen.cc/?replicaSet=repl0"
seeds:
- localhost.build.10gen.cc:27018
- localhost.build.10gen.cc:27019
hosts:
- localhost:27017
- localhost:27018
- localhost:27019
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"uri": "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0&connectTimeoutMS=250000",
"seeds": [
"localhost.build.10gen.cc:27017"
],
"hosts": [
"localhost:27017",
"localhost:27018",
"localhost:27019"
],
"options": {
"connectTimeoutMS": 250000,
"replicaSet": "repl0",
"socketTimeoutMS": 200000
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
uri: "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0&connectTimeoutMS=250000"
seeds:
- localhost.build.10gen.cc:27017
hosts:
- localhost:27017
- localhost:27018
- localhost:27019
options:
connectTimeoutMS: 250000
replicaSet: repl0
socketTimeoutMS: 200000
16 changes: 16 additions & 0 deletions data/initial-dns-seedlist-discovery/two-txt-records.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"uri": "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0",
"seeds": [
"localhost.build.10gen.cc:27017"
],
"hosts": [
"localhost:27017",
"localhost:27018",
"localhost:27019"
],
"options": {
"connectTimeoutMS": 200000,
"replicaSet": "repl0",
"socketTimeoutMS": 200000
}
}
11 changes: 11 additions & 0 deletions data/initial-dns-seedlist-discovery/two-txt-records.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
uri: "mongodb+srv://test6.test.build.10gen.cc/?replicaSet=repl0"
seeds:
- localhost.build.10gen.cc:27017
hosts:
- localhost:27017
- localhost:27018
- localhost:27019
options:
connectTimeoutMS: 200000
replicaSet: repl0
socketTimeoutMS: 200000
74 changes: 61 additions & 13 deletions mongo/connstring/connstring.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,21 @@ type parser struct {

func (p *parser) parse(original string) error {
p.Original = original

uri := original

var err error
// scheme
if !strings.HasPrefix(uri, "mongodb://") {
return fmt.Errorf("scheme must be \"mongodb\"")
var isSRV bool
if strings.HasPrefix(uri, "mongodb+srv://") {
isSRV = true
// remove the scheme
uri = uri[14:]
} else if strings.HasPrefix(uri, "mongodb://") {
// remove the scheme
uri = uri[10:]
} else {
return fmt.Errorf("scheme must be \"mongodb\" or \"mongodb+srv\"")
}

// user info
uri = uri[10:]

if idx := strings.Index(uri, "@"); idx != -1 {
userInfo := uri[:idx]
uri = uri[idx+1:]
Expand Down Expand Up @@ -128,10 +132,9 @@ func (p *parser) parse(original string) error {
return internal.WrapErrorf(err, "invalid password")
}
}

}

// hosts
// fetch the hosts field
hosts := uri
if idx := strings.IndexAny(uri, "/?@"); idx != -1 {
if uri[idx] == '@' {
Expand All @@ -140,17 +143,36 @@ func (p *parser) parse(original string) error {
if uri[idx] == '?' {
return fmt.Errorf("must have a / before the query ?")
}

hosts = uri[:idx]
}

for _, host := range strings.Split(hosts, ",") {
var connectionArgsFromTXT []string
parsedHosts := strings.Split(hosts, ",")

if isSRV {
parsedHosts = strings.Split(hosts, ",")
if len(parsedHosts) != 1 {
return fmt.Errorf("URI with SRV must include one and only one hostname")
}
parsedHosts, err = fetchSeedlistFromSRV(parsedHosts[0])
if err != nil {
return err
}

// error ignored because finding a TXT record should not be
// considered an error.
recordsFromTXT, _ := net.LookupTXT(hosts)
for _, recordFromTXT := range recordsFromTXT {
connectionArgsFromTXT = append(connectionArgsFromTXT, strings.FieldsFunc(recordFromTXT, func(r rune) bool { return r == ';' || r == '&' })...)
}
}

for _, host := range parsedHosts {
err = p.addHost(host)
if err != nil {
return internal.WrapErrorf(err, "invalid host \"%s\"", host)
}
}

if len(p.Hosts) == 0 {
return fmt.Errorf("must have at least 1 host")
}
Expand Down Expand Up @@ -195,7 +217,10 @@ func (p *parser) parse(original string) error {
return nil
}

for _, pair := range strings.FieldsFunc(uri, func(r rune) bool { return r == ';' || r == '&' }) {
connectionArgsFromQueryString := strings.FieldsFunc(uri, func(r rune) bool { return r == ';' || r == '&' })
connectionArgPairs := append(connectionArgsFromTXT, connectionArgsFromQueryString...)

for _, pair := range connectionArgPairs {
err = p.addOption(pair)
if err != nil {
return err
Expand All @@ -205,6 +230,29 @@ func (p *parser) parse(original string) error {
return nil
}

func fetchSeedlistFromSRV(host string) ([]string, error) {
var err error

_, _, err = net.SplitHostPort(host)

if err == nil {
// we were able to successfully extract a port from the host,
// but should not be able to when using SRV
return nil, fmt.Errorf("URI with srv must not include a port number")
}

_, addresses, err := net.LookupSRV("mongodb", "tcp", host)
if err != nil {
return nil, err
}
parsedHosts := make([]string, len(addresses))
for i, address := range addresses {
parsedHosts[i] = fmt.Sprintf("%s:%d", strings.TrimSuffix(address.Target, "."), address.Port)
}

return parsedHosts, nil
}

func (p *parser) addHost(host string) error {
if host == "" {
return nil
Expand Down
Loading

0 comments on commit aec5f69

Please sign in to comment.