diff --git a/build/test-lua.sh b/build/test-lua.sh index 90d4e8c74..0663a9b0c 100755 --- a/build/test-lua.sh +++ b/build/test-lua.sh @@ -23,6 +23,7 @@ resty \ -I /usr/local/lib/lua \ -I /usr/lib/lua-platform-path/lua/5.1 \ --shdict "configuration_data 5M" \ + --shdict "certificate_data 16M" \ --shdict "balancer_ewma 1M" \ --shdict "balancer_ewma_last_touched_at 1M" \ ./rootfs/etc/nginx/lua/test/run.lua ${BUSTED_ARGS} ./rootfs/etc/nginx/lua/test/ diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index 727fa4736..6883c0901 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -192,6 +192,7 @@ func buildLuaSharedDictionaries(s interface{}, dynamicConfigurationEnabled bool, if dynamicConfigurationEnabled { out = append(out, "lua_shared_dict configuration_data 5M", + "lua_shared_dict certificate_data 16M", "lua_shared_dict locks 512k", "lua_shared_dict balancer_ewma 1M", "lua_shared_dict balancer_ewma_last_touched_at 1M", diff --git a/rootfs/etc/nginx/lua/configuration.lua b/rootfs/etc/nginx/lua/configuration.lua index a8b58feff..189879654 100644 --- a/rootfs/etc/nginx/lua/configuration.lua +++ b/rootfs/etc/nginx/lua/configuration.lua @@ -1,5 +1,8 @@ +local json = require("cjson") + -- this is the Lua representation of Configuration struct in internal/ingress/types.go local configuration_data = ngx.shared.configuration_data +local certificate_data = ngx.shared.certificate_data local _M = { nameservers = {} @@ -29,6 +32,55 @@ local function fetch_request_body() return body end +function _M.get_pem_cert_key(hostname) + return certificate_data:get(hostname) +end + +local function handle_servers() + if ngx.var.request_method ~= "POST" then + ngx.status = ngx.HTTP_BAD_REQUEST + ngx.print("Only POST requests are allowed!") + return + end + + local raw_servers = fetch_request_body() + + local ok, servers = pcall(json.decode, raw_servers) + if not ok then + ngx.log(ngx.ERR, "could not parse servers: " .. tostring(servers)) + ngx.status = ngx.HTTP_BAD_REQUEST + return + end + + local err_buf = {} + for _, server in ipairs(servers) do + if server.hostname and server.sslCert.pemCertKey then + local success, err = certificate_data:safe_set(server.hostname, server.sslCert.pemCertKey) + if not success then + if err == "no memory" then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.log(ngx.ERR, "no memory in certificate_data dictionary") + return + end + + local err_msg = string.format("error setting certificate for %s: %s\n", + server.hostname, tostring(err)) + table.insert(err_buf, err_msg) + end + else + ngx.log(ngx.WARN, "hostname or pemCertKey are not present") + end + end + + if #err_buf > 0 then + ngx.log(ngx.ERR, table.concat(err_buf)) + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + return + end + + ngx.status = ngx.HTTP_CREATED +end + function _M.call() if ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "GET" then ngx.status = ngx.HTTP_BAD_REQUEST @@ -36,6 +88,11 @@ function _M.call() return end + if ngx.var.request_uri == "/configuration/servers" then + handle_servers() + return + end + if ngx.var.request_uri ~= "/configuration/backends" then ngx.status = ngx.HTTP_NOT_FOUND ngx.print("Not found!") @@ -65,4 +122,8 @@ function _M.call() ngx.status = ngx.HTTP_CREATED end +if _TEST then + _M.handle_servers = handle_servers +end + return _M diff --git a/rootfs/etc/nginx/lua/test/configuration_test.lua b/rootfs/etc/nginx/lua/test/configuration_test.lua new file mode 100644 index 000000000..28afb784a --- /dev/null +++ b/rootfs/etc/nginx/lua/test/configuration_test.lua @@ -0,0 +1,131 @@ +_G._TEST = true +local cjson = require("cjson") +local configuration = require("configuration") + +local unmocked_ngx = _G.ngx +local certificate_data = ngx.shared.certificate_data + +function get_mocked_ngx_env() + local _ngx = {} + setmetatable(_ngx, {__index = _G.ngx}) + + _ngx.status = 100 + _ngx.var = {} + _ngx.req = { + read_body = function() end, + get_body_file = function() end, + } + return _ngx +end + +describe("Configuration", function() + before_each(function() + _G.ngx = get_mocked_ngx_env() + end) + + after_each(function() + _G.ngx = unmocked_ngx + end) + + describe("handle_servers()", function() + it("should not accept non POST methods", function() + ngx.var.request_method = "GET" + + local s = spy.on(ngx, "print") + assert.has_no.errors(configuration.handle_servers) + assert.spy(s).was_called_with("Only POST requests are allowed!") + assert.same(ngx.status, ngx.HTTP_BAD_REQUEST) + end) + + it("should ignore servers that don't have hostname or pemCertKey set", function() + ngx.var.request_method = "POST" + local mock_servers = cjson.encode({ + { + hostname = "hostname", + sslCert = {} + }, + { + sslCert = { + pemCertKey = "pemCertKey" + } + } + }) + ngx.req.get_body_data = function() return mock_servers end + + local s = spy.on(ngx, "log") + assert.has_no.errors(configuration.handle_servers) + assert.spy(s).was_called_with(ngx.WARN, "hostname or pemCertKey are not present") + assert.same(ngx.status, ngx.HTTP_CREATED) + end) + + it("should successfully update certificates and keys for each host", function() + ngx.var.request_method = "POST" + local mock_servers = cjson.encode({ + { + hostname = "hostname", + sslCert = { + pemCertKey = "pemCertKey" + } + } + }) + ngx.req.get_body_data = function() return mock_servers end + + assert.has_no.errors(configuration.handle_servers) + assert.same(certificate_data:get("hostname"), "pemCertKey") + assert.same(ngx.status, ngx.HTTP_CREATED) + end) + + it("should log an err and set status to Internal Server Error when a certificate cannot be set", function() + ngx.var.request_method = "POST" + ngx.shared.certificate_data.safe_set = function(self, data) return false, "error" end + local mock_servers = cjson.encode({ + { + hostname = "hostname", + sslCert = { + pemCertKey = "pemCertKey" + } + }, + { + hostname = "hostname2", + sslCert = { + pemCertKey = "pemCertKey2" + } + } + }) + ngx.req.get_body_data = function() return mock_servers end + + local s = spy.on(ngx, "log") + assert.has_no.errors(configuration.handle_servers) + assert.spy(s).was_called_with(ngx.ERR, + "error setting certificate for hostname: error\nerror setting certificate for hostname2: error\n") + assert.same(ngx.status, ngx.HTTP_INTERNAL_SERVER_ERROR) + end) + + it("should log an err, set status to Internal Server Error, and short circuit when shared dictionary is full", function() + ngx.var.request_method = "POST" + ngx.shared.certificate_data.safe_set = function(self, data) return false, "no memory" end + local mock_servers = cjson.encode({ + { + hostname = "hostname", + sslCert = { + pemCertKey = "pemCertKey" + } + }, + { + hostname = "hostname2", + sslCert = { + pemCertKey = "pemCertKey2" + } + } + }) + ngx.req.get_body_data = function() return mock_servers end + + local s1 = spy.on(ngx, "log") + local s2 = spy.on(ngx.shared.certificate_data, "safe_set") + assert.has_no.errors(configuration.handle_servers) + assert.spy(s1).was_called_with(ngx.ERR, "no memory in certificate_data dictionary") + assert.spy(s2).was_not_called_with("hostname2", "pemCertKey2") + assert.same(ngx.status, ngx.HTTP_INTERNAL_SERVER_ERROR) + end) + end) +end)