Repository: chef/automate
Pull request #4454 (2020-11-30 12:22:13)

Resolve config parser
### :nut_and_bolt: Description: Added parser to parse resolv.conf file or similar file to get nameservers
This is required for the customer issue 268
<!-- /!\ Please ensure that you are NOT disclosing any customer information without their consent /!\ -->

### :chains: Related Resources

### :+1: Definition of Done 
The nameservers should be fetched from /etc/resolv.conf file

### :athletic_shoe: How to Build and Test the Change

### :white_check_mark: Checklist

- [x] I have read the [CONTRIBUTING document](https://github.com/chef/automate/blob/master/CONTRIBUTING.md).
- [x] Tests added/updated?
- [ ] Docs added/updated?
- [x] All commits have been signed-off for the [Developer Certification of Origin](https://github.com/chef/chef/blob/master/CONTRIBUTING.md#developer-certification-of-origin-dco).

### :camera: Screenshots, if applicable


netlify[bot]: Deploy preview for *chef-automate* processing.

Building with commit 07c4746fdb886ab3c64f788dce2ba0219196ec84

https://app.netlify.com/sites/chef-automate/deploys/5fc4e3f76b13c70008aa38e2

Patch:
diff --git a/lib/config/resolve_config_parser.go b/lib/config/resolve_config_parser.go
new file mode 100644
index 000000000..03b129c2c
--- /dev/null
+++ b/lib/config/resolve_config_parser.go
@@ -0,0 +1,68 @@
+package config
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net"
+	"regexp"
+)
+
+var (
+	defaultResolvConf = "/etc/resolv.conf"
+	nameServerRegex   = regexp.MustCompile(`^\s*nameserver\s*(.+?)\s*$`)
+)
+
+// GetNameServersFromResolveConfig parses a file in the format of /etc/resolv.conf
+// The resolve.conf format is described in resolv.conf(5):
+//
+//    https://man7.org/linux/man-pages/man5/resolv.conf.5.html
+//
+// This function looks up at the lines starting with `nameserver`.
+// There can be a single nameserver each line and maximum 3 (defined
+// my MAXNS in resolv.h) in the file.
+func GetNameServersFromResolveConfig(filePath string) ([]string, error) {
+	content, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		return nil, err
+	}
+
+	nameservers := ParseNameServers(content)
+	return nameservers, nil
+}
+
+// ParseNameServers parses an array of bytes and returns the nameservers
+// The content should be of format of /etc/resolv.conf i.e.
+// nameserver 12.0.0.3
+func ParseNameServers(fileContent []byte) []string {
+	fileLines := bytes.Split(fileContent, []byte("\n"))
+	var nameservers []string
+	for _, currentLine := range fileLines {
+		var contentToParse = currentLine
+		var commentIndicatorIndex = bytes.Index(currentLine, []byte("#"))
+		if commentIndicatorIndex != -1 {
+			// Only check the content before the comment section
+			contentToParse = currentLine[:commentIndicatorIndex]
+		}
+
+		server := nameServerRegex.FindSubmatch(contentToParse)
+		if len(server) == 2 {
+			address := string(server[1])
+			if net.ParseIP(address) != nil {
+				nameservers = append(nameservers, address)
+			}
+		}
+	}
+
+	return nameservers
+}
+
+// GetSystemResolvers returns resolvers discovered via
+// /etc/resolv.conf. If not valid resolvers are found, an empty array
+// is returned.
+func GetSystemResolvers() []string {
+	resolvers, err := GetNameServersFromResolveConfig(defaultResolvConf)
+	if err != nil {
+		return []string{}
+	}
+	return resolvers
+}


Test patch:
diff --git a/lib/config/resolve_config_parser_test.go b/lib/config/resolve_config_parser_test.go
new file mode 100644
index 000000000..655ae9989
--- /dev/null
+++ b/lib/config/resolve_config_parser_test.go
@@ -0,0 +1,81 @@
+package config_test
+
+import (
+	"testing"
+
+	"github.com/chef/automate/lib/config"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseNameServers(t *testing.T) {
+	cases := []struct {
+		desc     string
+		content  []byte
+		expected []string
+	}{
+		{
+			desc: "parses IPv4 nameserver",
+			content: []byte(`
+nameserver 10.0.0.3
+	`),
+			expected: []string{"10.0.0.3"},
+		},
+		{
+			desc: "parses IPv6 nameserver",
+			content: []byte(`
+	nameserver 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+	`),
+			expected: []string{"2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
+		},
+		{
+			desc: "parses spaces between nameserver and IP",
+			content: []byte(`
+nameserver                          10.0.0.3
+	`),
+			expected: []string{"10.0.0.3"},
+		},
+		{
+			desc: "parses multiple nameservers",
+			content: []byte(`
+	nameserver 10.0.0.3
+	nameserver 10.0.0.4
+	`),
+			expected: []string{"10.0.0.3", "10.0.0.4"},
+		},
+		{
+			desc: "parses content with single-line comments",
+			content: []byte(`
+	# Here goes the name servers
+	nameserver 10.0.0.3
+	`),
+			expected: []string{"10.0.0.3"},
+		},
+		{
+			desc: "parses content with same-line comments",
+			content: []byte(`
+	nameserver 10.0.0.3 # Here goes the name servers
+	`),
+			expected: []string{"10.0.0.3"},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.desc, func(t *testing.T) {
+			ns := config.ParseNameServers(c.content)
+			assert.Equal(t, c.expected, ns)
+
+		})
+	}
+}
+
+func TestGetNameServersFromResolveConfig(t *testing.T) {
+	var filePath = "./testdata/sample_resolv_conf"
+	ns, err := config.GetNameServersFromResolveConfig(filePath)
+	require.NoError(t, err, "failed to parse config")
+
+	var expectedResult = []string{"255.0.0.1"}
+
+	require.Equal(t, expectedResult, ns)
+}
diff --git a/lib/config/testdata/sample_resolv_conf b/lib/config/testdata/sample_resolv_conf
new file mode 100644
index 000000000..d2ac5fd1d
--- /dev/null
+++ b/lib/config/testdata/sample_resolv_conf
@@ -0,0 +1,19 @@
+# This file is managed by man:systemd-resolved(8). Do not edit.
+#
+# This is a dynamic resolv.conf file for connecting local clients to the
+# internal DNS stub resolver of systemd-resolved. This file lists all
+# configured search domains.
+#
+# Run "resolvectl status" to see details about the uplink DNS servers
+# currently in use.
+#
+# Third party programs must not access this file directly, but only through the
+# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
+# replace this symlink by a static file or a different symlink.
+#
+# See man:systemd-resolved.service(8) for details about the supported modes of
+# operation for /etc/resolv.conf.
+
+nameserver 255.0.0.1
+options edns0
+search us-west-2.compute.internal
\ No newline at end of file