ipam/host-local: add ResolvConf argument for DNS configuration
authorCasey Callendrello <c1@caseyc.net>
Mon, 21 Nov 2016 18:05:41 +0000 (19:05 +0100)
committerCasey Callendrello <casey.Callendrello@coreos.com>
Wed, 11 Jan 2017 17:48:35 +0000 (18:48 +0100)
This adds the option `resolvConf` to the host-local IPAM configuration.
If specified, the plugin will try to parse the file as a resolv.conf(5)
type file and return it in the DNS response.

Documentation/host-local.md
plugins/ipam/host-local/README.md
plugins/ipam/host-local/backend/allocator/config.go
plugins/ipam/host-local/dns.go [new file with mode: 0644]
plugins/ipam/host-local/dns_test.go [new file with mode: 0644]
plugins/ipam/host-local/host_local_test.go
plugins/ipam/host-local/main.go

index 3744615..00c1e84 100644 (file)
@@ -30,6 +30,7 @@ It stores the state locally on the host filesystem, therefore ensuring uniquenes
 * `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block.
 * `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
 * `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
+* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
 
 ## Supported arguments
 The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
index 2efd210..128bc6d 100644 (file)
@@ -1,6 +1,7 @@
 # host-local IP address manager
 
-host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range.
+host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range. Optionally,
+it can include a DNS configuration from a `resolv.conf` file on the host.
 
 ## Usage
 
@@ -65,7 +66,8 @@ f81d4fae-7dec-11d0-a765-00a0c91e6bf6
                "rangeEnd": "3ffe:ffff:0:01ff::0020",
                "routes": [
                        { "dst": "3ffe:ffff:0:01ff::1/64" }
-               ]
+               ],
+               "resolvConf": "/etc/resolv.conf"
        }
 }
 ```
index 6e5e4ab..cf80ac2 100644 (file)
@@ -32,6 +32,7 @@ type IPAMConfig struct {
        Gateway    net.IP        `json:"gateway"`
        Routes     []types.Route `json:"routes"`
        DataDir    string        `json:"dataDir"`
+       ResolvConf string        `json:"resolvConf"`
        Args       *IPAMArgs     `json:"-"`
 }
 
diff --git a/plugins/ipam/host-local/dns.go b/plugins/ipam/host-local/dns.go
new file mode 100644 (file)
index 0000000..1b3975a
--- /dev/null
@@ -0,0 +1,64 @@
+// Copyright 2016 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "bufio"
+       "os"
+       "strings"
+
+       "github.com/containernetworking/cni/pkg/types"
+)
+
+// parseResolvConf parses an existing resolv.conf in to a DNS struct
+func parseResolvConf(filename string) (*types.DNS, error) {
+       fp, err := os.Open(filename)
+       if err != nil {
+               return nil, err
+       }
+
+       dns := types.DNS{}
+       scanner := bufio.NewScanner(fp)
+       for scanner.Scan() {
+               line := scanner.Text()
+               line = strings.TrimSpace(line)
+
+               // Skip comments, empty lines
+               if len(line) == 0 || line[0] == '#' || line[0] == ';' {
+                       continue
+               }
+
+               fields := strings.Fields(line)
+               if len(fields) < 2 {
+                       continue
+               }
+               switch fields[0] {
+               case "nameserver":
+                       dns.Nameservers = append(dns.Nameservers, fields[1])
+               case "domain":
+                       dns.Domain = fields[1]
+               case "search":
+                       dns.Search = append(dns.Search, fields[1:]...)
+               case "options":
+                       dns.Options = append(dns.Options, fields[1:]...)
+               }
+       }
+
+       if err := scanner.Err(); err != nil {
+               return nil, err
+       }
+
+       return &dns, nil
+}
diff --git a/plugins/ipam/host-local/dns_test.go b/plugins/ipam/host-local/dns_test.go
new file mode 100644 (file)
index 0000000..4f3a05f
--- /dev/null
@@ -0,0 +1,80 @@
+// Copyright 2016 CNI authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+       "io/ioutil"
+       "os"
+
+       "github.com/containernetworking/cni/pkg/types"
+       . "github.com/onsi/ginkgo"
+       . "github.com/onsi/gomega"
+)
+
+var _ = Describe("parsing resolv.conf", func() {
+       It("parses a simple resolv.conf file", func() {
+               contents := `
+               nameserver 192.0.2.0
+               nameserver 192.0.2.1
+               `
+               dns, err := parse(contents)
+               Expect(err).NotTo(HaveOccurred())
+               Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0", "192.0.2.1"}}))
+       })
+       It("ignores comments", func() {
+               dns, err := parse(`
+nameserver 192.0.2.0
+;nameserver 192.0.2.1
+`)
+               Expect(err).NotTo(HaveOccurred())
+               Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0"}}))
+       })
+       It("parses all fields", func() {
+               dns, err := parse(`
+nameserver 192.0.2.0
+nameserver 192.0.2.2
+domain example.com
+;nameserver comment
+#nameserver comment
+search example.net example.org
+search example.gov
+options one two three
+options four
+`)
+               Expect(err).NotTo(HaveOccurred())
+               Expect(*dns).Should(Equal(types.DNS{
+                       Nameservers: []string{"192.0.2.0", "192.0.2.2"},
+                       Domain:      "example.com",
+                       Search:      []string{"example.net", "example.org", "example.gov"},
+                       Options:     []string{"one", "two", "three", "four"},
+               }))
+       })
+})
+
+func parse(contents string) (*types.DNS, error) {
+       f, err := ioutil.TempFile("", "host_local_resolv")
+       defer f.Close()
+       defer os.Remove(f.Name())
+
+       if err != nil {
+               return nil, err
+       }
+
+       if _, err := f.WriteString(contents); err != nil {
+               return nil, err
+       }
+
+       return parseResolvConf(f.Name())
+}
index 97b3b6f..01906bb 100644 (file)
@@ -38,6 +38,9 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
                defer os.RemoveAll(tmpDir)
 
+               err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
+               Expect(err).NotTo(HaveOccurred())
+
                conf := fmt.Sprintf(`{
     "cniVersion": "0.2.0",
     "name": "mynet",
@@ -46,9 +49,10 @@ var _ = Describe("host-local Operations", func() {
     "ipam": {
         "type": "host-local",
         "subnet": "10.1.2.0/24",
-        "dataDir": "%s"
+        "dataDir": "%s",
+               "resolvConf": "%s/resolv.conf"
     }
-}`, tmpDir)
+}`, tmpDir, tmpDir)
 
                args := &skel.CmdArgs{
                        ContainerID: "dummy",
@@ -80,6 +84,8 @@ var _ = Describe("host-local Operations", func() {
                Expect(err).NotTo(HaveOccurred())
                Expect(string(contents)).To(Equal("10.1.2.2"))
 
+               Expect(result.DNS).To(Equal(types.DNS{Nameservers: []string{"192.0.2.3"}}))
+
                // Release the IP
                err = testutils.CmdDelWithResult(nspath, ifname, func() error {
                        return cmdDel(args)
index 330d9ac..287c0a0 100644 (file)
@@ -33,6 +33,16 @@ func cmdAdd(args *skel.CmdArgs) error {
                return err
        }
 
+       r := types.Result{}
+
+       if ipamConf.ResolvConf != "" {
+               dns, err := parseResolvConf(ipamConf.ResolvConf)
+               if err != nil {
+                       return err
+               }
+               r.DNS = *dns
+       }
+
        store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
        if err != nil {
                return err
@@ -48,10 +58,8 @@ func cmdAdd(args *skel.CmdArgs) error {
        if err != nil {
                return err
        }
+       r.IP4 = ipConf
 
-       r := &types.Result{
-               IP4: ipConf,
-       }
        return r.Print()
 }