From 27296caeb8bc4335b269d171d8f3ac2df81e4686 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Fri, 21 Aug 2015 17:31:31 -0700 Subject: [PATCH] Add DNS 'options' support This is needed to expose DNS options like 'ndots' into containers. https://github.com/docker/docker/issues/14069 Signed-off-by: Tim Hockin --- libnetwork/resolvconf/resolvconf.go | 29 ++++++++++-- libnetwork/resolvconf/resolvconf_test.go | 59 ++++++++++++++++++++++-- libnetwork/sandbox.go | 17 ++++++- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/libnetwork/resolvconf/resolvconf.go b/libnetwork/resolvconf/resolvconf.go index b6fd51c06f..aa710ebfbd 100644 --- a/libnetwork/resolvconf/resolvconf.go +++ b/libnetwork/resolvconf/resolvconf.go @@ -30,6 +30,7 @@ var ( nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`) nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`) searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) + optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`) ) var lastModified struct { @@ -165,10 +166,25 @@ func GetSearchDomains(resolvConf []byte) []string { return domains } +// GetOptions returns options (if any) listed in /etc/resolv.conf +// If more than one options line is encountered, only the contents of the last +// one is returned. +func GetOptions(resolvConf []byte) []string { + options := []string{} + for _, line := range getLines(resolvConf, []byte("#")) { + match := optionsRegexp.FindSubmatch(line) + if match == nil { + continue + } + options = strings.Fields(string(match[1])) + } + return options +} + // Build writes a configuration file to path containing a "nameserver" entry -// for every element in dns, and a "search" entry for every element in -// dnsSearch. -func Build(path string, dns, dnsSearch []string) (string, error) { +// for every element in dns, a "search" entry for every element in +// dnsSearch, and an "options" entry for every element in dnsOptions. +func Build(path string, dns, dnsSearch, dnsOptions []string) (string, error) { content := bytes.NewBuffer(nil) if len(dnsSearch) > 0 { if searchString := strings.Join(dnsSearch, " "); strings.Trim(searchString, " ") != "." { @@ -182,6 +198,13 @@ func Build(path string, dns, dnsSearch []string) (string, error) { return "", err } } + if len(dnsOptions) > 0 { + if optsString := strings.Join(dnsOptions, " "); strings.Trim(optsString, " ") != "" { + if _, err := content.WriteString("options " + optsString + "\n"); err != nil { + return "", err + } + } + } hash, err := ioutils.HashData(bytes.NewReader(content.Bytes())) if err != nil { diff --git a/libnetwork/resolvconf/resolvconf_test.go b/libnetwork/resolvconf/resolvconf_test.go index 44881ab963..74f30e4889 100644 --- a/libnetwork/resolvconf/resolvconf_test.go +++ b/libnetwork/resolvconf/resolvconf_test.go @@ -98,6 +98,32 @@ nameserver 4.30.20.100`: {"foo.example.com", "example.com"}, } } +func TestGetOptions(t *testing.T) { + for resolv, result := range map[string][]string{ + `options opt1`: {"opt1"}, + `options opt1 # ignored`: {"opt1"}, + ` options opt1 `: {"opt1"}, + ` options opt1 # ignored`: {"opt1"}, + `options opt1 opt2 opt3`: {"opt1", "opt2", "opt3"}, + `options opt1 opt2 opt3 # ignored`: {"opt1", "opt2", "opt3"}, + ` options opt1 opt2 opt3 `: {"opt1", "opt2", "opt3"}, + ` options opt1 opt2 opt3 # ignored`: {"opt1", "opt2", "opt3"}, + ``: {}, + `# ignored`: {}, + `nameserver 1.2.3.4`: {}, + `nameserver 1.2.3.4 +options opt1 opt2 opt3`: {"opt1", "opt2", "opt3"}, + `nameserver 1.2.3.4 +options opt1 opt2 +options opt3 opt4`: {"opt3", "opt4"}, + } { + test := GetOptions([]byte(resolv)) + if !strSlicesEqual(test, result) { + t.Fatalf("Wrong options string {%s} should be %v. Input: %s", test, result, resolv) + } + } +} + func strSlicesEqual(a, b []string) bool { if len(a) != len(b) { return false @@ -119,7 +145,7 @@ func TestBuild(t *testing.T) { } defer os.Remove(file.Name()) - _, err = Build(file.Name(), []string{"ns1", "ns2", "ns3"}, []string{"search1"}) + _, err = Build(file.Name(), []string{"ns1", "ns2", "ns3"}, []string{"search1"}, []string{"opt1"}) if err != nil { t.Fatal(err) } @@ -129,7 +155,7 @@ func TestBuild(t *testing.T) { t.Fatal(err) } - if expected := "search search1\nnameserver ns1\nnameserver ns2\nnameserver ns3\n"; !bytes.Contains(content, []byte(expected)) { + if expected := "search search1\nnameserver ns1\nnameserver ns2\nnameserver ns3\noptions opt1\n"; !bytes.Contains(content, []byte(expected)) { t.Fatalf("Expected to find '%s' got '%s'", expected, content) } } @@ -141,7 +167,7 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) { } defer os.Remove(file.Name()) - _, err = Build(file.Name(), []string{"ns1", "ns2", "ns3"}, []string{"."}) + _, err = Build(file.Name(), []string{"ns1", "ns2", "ns3"}, []string{"."}, []string{"opt1"}) if err != nil { t.Fatal(err) } @@ -151,7 +177,32 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) { t.Fatal(err) } - if expected := "nameserver ns1\nnameserver ns2\nnameserver ns3\n"; !bytes.Contains(content, []byte(expected)) { + if expected := "nameserver ns1\nnameserver ns2\nnameserver ns3\noptions opt1\n"; !bytes.Contains(content, []byte(expected)) { + t.Fatalf("Expected to find '%s' got '%s'", expected, content) + } + if notExpected := "search ."; bytes.Contains(content, []byte(notExpected)) { + t.Fatalf("Expected to not find '%s' got '%s'", notExpected, content) + } +} + +func TestBuildWithNoOptions(t *testing.T) { + file, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(file.Name()) + + _, err = Build(file.Name(), []string{"ns1", "ns2", "ns3"}, []string{"search1"}, []string{}) + if err != nil { + t.Fatal(err) + } + + content, err := ioutil.ReadFile(file.Name()) + if err != nil { + t.Fatal(err) + } + + if expected := "search search1\nnameserver ns1\nnameserver ns2\nnameserver ns3\n"; !bytes.Contains(content, []byte(expected)) { t.Fatalf("Expected to find '%s' got '%s'", expected, content) } if notExpected := "search ."; bytes.Contains(content, []byte(notExpected)) { diff --git a/libnetwork/sandbox.go b/libnetwork/sandbox.go index 0d1351393b..69c31989a3 100644 --- a/libnetwork/sandbox.go +++ b/libnetwork/sandbox.go @@ -93,6 +93,7 @@ type resolvConfPathConfig struct { resolvConfHashFile string dnsList []string dnsSearchList []string + dnsOptionsList []string } type containerConfig struct { @@ -406,17 +407,21 @@ func (sb *sandbox) setupDNS() error { } dnsList := resolvconf.GetNameservers(resolvConf) dnsSearchList := resolvconf.GetSearchDomains(resolvConf) + dnsOptionsList := resolvconf.GetOptions(resolvConf) - if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 { + if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 || len(dnsOptionsList) > 0 { if len(sb.config.dnsList) > 0 { dnsList = sb.config.dnsList } if len(sb.config.dnsSearchList) > 0 { dnsSearchList = sb.config.dnsSearchList } + if len(sb.config.dnsOptionsList) > 0 { + dnsOptionsList = sb.config.dnsOptionsList + } } - hash, err := resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList) + hash, err := resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList) if err != nil { return err } @@ -580,6 +585,14 @@ func OptionDNSSearch(search string) SandboxOption { } } +// OptionDNSOptions function returns an option setter for dns options entry option to +// be passed to container Create method. +func OptionDNSOptions(options string) SandboxOption { + return func(sb *sandbox) { + sb.config.dnsOptionsList = append(sb.config.dnsOptionsList, options) + } +} + // OptionUseDefaultSandbox function returns an option setter for using default sandbox to // be passed to container Create method. func OptionUseDefaultSandbox() SandboxOption {