Lua /etc/resolv.conf parser and some refactoring

This commit is contained in:
Elvin Efendi 2019-08-13 18:21:22 -04:00
parent 333d9fd48d
commit d46b4148fa
15 changed files with 224 additions and 124 deletions

View file

@ -4,9 +4,7 @@ local cjson = require("cjson.safe")
local configuration_data = ngx.shared.configuration_data
local certificate_data = ngx.shared.certificate_data
local _M = {
nameservers = {}
}
local _M = {}
function _M.get_backends_data()
return configuration_data:get("backends")

View file

@ -315,8 +315,7 @@ describe("Balancer", function()
}
}
local dns_helper = require("test/dns_helper")
dns_helper.mock_dns_query({
helpers.mock_resty_dns_query({
{
name = "example.com",
address = "192.168.1.1",

View file

@ -115,15 +115,20 @@ describe("Configuration", function()
end)
it("returns a status of 400", function()
local original_io_open = _G.io.open
_G.io.open = function(filename, extension) return false end
assert.has_no.errors(configuration.call)
assert.equal(ngx.status, ngx.HTTP_BAD_REQUEST)
_G.io.open = original_io_open
end)
it("logs 'dynamic-configuration: unable to read valid request body to stderr'", function()
local original_io_open = _G.io.open
_G.io.open = function(filename, extension) return false end
local s = spy.on(ngx, "log")
assert.has_no.errors(configuration.call)
assert.spy(s).was_called_with(ngx.ERR, "dynamic-configuration: unable to read valid request body")
_G.io.open = original_io_open
end)
end)

View file

@ -1,27 +0,0 @@
local _M = {}
local configuration = require("configuration")
local resolver = require("resty.dns.resolver")
local old_resolver_new = resolver.new
local function reset(nameservers)
configuration.nameservers = nameservers or { "1.1.1.1" }
end
function _M.mock_new(func, nameservers)
reset(nameservers)
resolver.new = func
end
function _M.mock_dns_query(response, err)
reset()
resolver.new = function(self, options)
local r = old_resolver_new(self, options)
r.query = function(self, name, options, tries)
return response, err
end
return r
end
end
return _M

View file

@ -0,0 +1,47 @@
local _M = {}
local resty_dns_resolver = require("resty.dns.resolver")
local original_resty_dns_resolver_new = resty_dns_resolver.new
local original_io_open = io.open
function _M.with_resolv_conf(content, func)
local new_resolv_conf_f = assert(io.tmpfile())
new_resolv_conf_f:write(content)
new_resolv_conf_f:seek("set", 0)
io.open = function(path, mode)
if path ~= "/etc/resolv.conf" then
error("expected '/etc/resolv.conf' as path but got: " .. tostring(path))
end
if mode ~= "r" then
error("expected 'r' as mode but got: " .. tostring(mode))
end
return new_resolv_conf_f, nil
end
func()
io.open = original_io_open
if io.type(new_resolv_conf_f) ~= "closed file" then
error("file was left open")
end
end
function _M.mock_resty_dns_new(func)
resty_dns_resolver.new = func
end
function _M.mock_resty_dns_query(response, err)
resty_dns_resolver.new = function(self, options)
local r = original_resty_dns_resolver_new(self, options)
r.query = function(self, name, options, tries)
return response, err
end
return r
end
end
return _M

View file

@ -11,6 +11,7 @@ do
-- if there's more constants need to be whitelisted for test runs, add here.
local GLOBALS_ALLOWED_IN_TEST = {
_TEST = true,
helpers = true,
}
local newindex = function(table, key, value)
rawset(table, key, value)
@ -33,6 +34,8 @@ do
setmetatable(_G, { __newindex = newindex })
end
_G.helpers = require("test.helpers")
local ffi = require("ffi")
local lua_ingress = require("lua_ingress")

View file

@ -1,41 +1,51 @@
local conf = [===[
nameserver 1.2.3.4
nameserver 4.5.6.7
search ingress-nginx.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
]===]
helpers.with_resolv_conf(conf, function()
require("util.resolv_conf")
end)
describe("resolve", function()
local dns = require("util.dns")
local dns_helper = require("test/dns_helper")
it("sets correct nameservers", function()
dns_helper.mock_new(function(self, options)
helpers.mock_resty_dns_new(function(self, options)
assert.are.same({ nameservers = { "1.2.3.4", "4.5.6.7" }, retrans = 5, timeout = 2000 }, options)
return nil, ""
end, { "1.2.3.4", "4.5.6.7" })
end)
dns.resolve("example.com")
end)
it("returns host when an error happens", function()
local s_ngx_log = spy.on(ngx, "log")
dns_helper.mock_new(function(...) return nil, "an error" end)
helpers.mock_resty_dns_new(function(...) return nil, "an error" end)
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to instantiate the resolver: an error")
dns_helper.mock_dns_query(nil, "oops!")
helpers.mock_resty_dns_query(nil, "oops!")
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\noops!\noops!")
dns_helper.mock_dns_query({ errcode = 1, errstr = "format error" })
helpers.mock_resty_dns_query({ errcode = 1, errstr = "format error" })
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nserver returned error code: 1: format error\nserver returned error code: 1: format error")
dns_helper.mock_dns_query({})
helpers.mock_resty_dns_query({})
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nno record resolved\nno record resolved")
dns_helper.mock_dns_query({ { name = "example.com", cname = "sub.example.com", ttl = 60 } })
helpers.mock_resty_dns_query({ { name = "example.com", cname = "sub.example.com", ttl = 60 } })
assert.are.same({ "example.com" }, dns.resolve("example.com"))
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "failed to query the DNS server:\nno record resolved\nno record resolved")
end)
it("resolves all A records of given host, caches them with minimal ttl and returns from cache next time", function()
dns_helper.mock_dns_query({
helpers.mock_resty_dns_query({
{
name = "example.com",
address = "192.168.1.1",
@ -66,7 +76,7 @@ describe("resolve", function()
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns.resolve("example.com"))
dns_helper.mock_new(function(...)
helpers.mock_resty_dns_new(function(...)
error("expected to short-circuit and return response from cache")
end)
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns.resolve("example.com"))

View file

@ -0,0 +1,64 @@
local original_io_open = io.open
describe("resolv_conf", function()
after_each(function()
package.loaded["util.resolv_conf"] = nil
io.open = original_io_open
end)
it("errors when file can not be opened", function()
io.open = function(...)
return nil, "file does not exist"
end
assert.has_error(function() require("util.resolv_conf") end, "could not open /etc/resolv.conf: file does not exist")
end)
it("opens '/etc/resolv.conf' with mode 'r'", function()
io.open = function(path, mode)
assert.are.same("/etc/resolv.conf", path)
assert.are.same("r", mode)
return original_io_open(path, mode)
end
assert.has_no.errors(function() require("util.resolv_conf") end)
end)
it("correctly parses resolv.conf", function()
local conf = [===[
# This is a comment
nameserver 10.96.0.10
nameserver 10.96.0.99
search ingress-nginx.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
]===]
helpers.with_resolv_conf(conf, function()
local resolv_conf = require("util.resolv_conf")
assert.are.same({
nameservers = { "10.96.0.10", "10.96.0.99" },
search = { "ingress-nginx.svc.cluster.local", "svc.cluster.local", "cluster.local" },
ndots = 5,
}, resolv_conf)
end)
end)
it("ignores options that it does not understand", function()
local conf = [===[
nameserver 10.96.0.10
search example.com
options debug
options ndots:3
]===]
helpers.with_resolv_conf(conf, function()
local resolv_conf = require("util.resolv_conf")
assert.are.same({
nameservers = { "10.96.0.10" },
search = { "example.com" },
ndots = 3,
}, resolv_conf)
end)
end)
end)

View file

@ -1,7 +1,6 @@
local resolver = require("resty.dns.resolver")
local lrucache = require("resty.lrucache")
local configuration = require("configuration")
local util = require("util")
local resolv_conf = require("util.resolv_conf")
local _M = {}
local CACHE_SIZE = 10000
@ -59,7 +58,7 @@ function _M.resolve(host)
local r
r, err = resolver:new{
nameservers = util.deepcopy(configuration.nameservers),
nameservers = resolv_conf.nameservers,
retrans = 5,
timeout = 2000, -- 2 sec
}

View file

@ -0,0 +1,79 @@
local ngx_re_split = require("ngx.re").split
local ngx_log = ngx.log
local ngx_ERR = ngx.ERR
local CONF_PATH = "/etc/resolv.conf"
local nameservers, search, ndots = {}, {}, 1
local function set_search(parts)
local length = #parts
for i = 2, length, 1 do
search[i-1] = parts[i]
end
end
local function set_ndots(parts)
local option = parts[2]
if not option then
return
end
local option_parts, err = ngx_re_split(option, ":")
if err then
ngx_log(ngx_ERR, err)
return
end
if option_parts[1] ~= "ndots" then
return
end
ndots = tonumber(option_parts[2])
end
local function is_comment(line)
return line:sub(1, 1) == "#"
end
local function parse_line(line)
if is_comment(line) then
return
end
local parts, err = ngx_re_split(line, "\\s+")
if err then
ngx_log(ngx_ERR, err)
end
local keyword, value = parts[1], parts[2]
if keyword == "nameserver" then
nameservers[#nameservers + 1] = value
elseif keyword == "search" then
set_search(parts)
elseif keyword == "options" then
set_ndots(parts)
end
end
do
local f, err = io.open(CONF_PATH, "r")
if not f then
error("could not open " .. CONF_PATH .. ": " .. tostring(err))
end
for line in f:lines() do
parse_line(line)
end
f:close()
end
return {
nameservers = nameservers,
search = search,
ndots = ndots,
}