Merge branch 'master' into xff
This commit is contained in:
commit
b5bcb93a4b
1532 changed files with 65966 additions and 34963 deletions
|
|
@ -22,14 +22,29 @@ RUN clean-install \
|
|||
diffutils \
|
||||
dumb-init
|
||||
|
||||
# Create symlinks to redirect nginx logs to stdout and stderr docker log collector
|
||||
# This only works if nginx is started with CMD or ENTRYPOINT
|
||||
RUN mkdir -p /var/log/nginx \
|
||||
&& ln -sf /dev/stdout /var/log/nginx/access.log \
|
||||
&& ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
|
||||
COPY . /
|
||||
|
||||
ENTRYPOINT ["/usr/bin/dumb-init"]
|
||||
# Fix permission during the build to avoid issues at runtime
|
||||
# with volumes (custom templates)
|
||||
RUN bash -eu -c ' \
|
||||
writeDirs=( \
|
||||
/etc/nginx/template \
|
||||
/etc/ingress-controller/ssl \
|
||||
/etc/ingress-controller/auth \
|
||||
/var/log \
|
||||
/var/log/nginx \
|
||||
/tmp \
|
||||
); \
|
||||
for dir in "${writeDirs[@]}"; do \
|
||||
mkdir -p ${dir}; \
|
||||
chown -R www-data.www-data ${dir}; \
|
||||
done' \
|
||||
&& chown www-data.www-data /etc/nginx/nginx.conf \
|
||||
&& chown www-data.www-data /etc/nginx/opentracing.json
|
||||
|
||||
# Create symlinks to redirect nginx logs to stdout and stderr docker log collector
|
||||
# This only works if nginx is started with CMD or ENTRYPOINT
|
||||
RUN ln -sf /dev/stdout /var/log/nginx/access.log
|
||||
RUN ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
|
||||
CMD ["/nginx-ingress-controller"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
local ngx_balancer = require("ngx.balancer")
|
||||
local json = require("cjson")
|
||||
local util = require("util")
|
||||
local dns_util = require("util.dns")
|
||||
local configuration = require("configuration")
|
||||
local round_robin = require("balancer.round_robin")
|
||||
local chash = require("balancer.chash")
|
||||
|
|
@ -40,6 +42,19 @@ local function get_implementation(backend)
|
|||
return implementation
|
||||
end
|
||||
|
||||
local function resolve_external_names(original_backend)
|
||||
local backend = util.deepcopy(original_backend)
|
||||
local endpoints = {}
|
||||
for _, endpoint in ipairs(backend.endpoints) do
|
||||
local ips = dns_util.resolve(endpoint.address)
|
||||
for _, ip in ipairs(ips) do
|
||||
table.insert(endpoints, { address = ip, port = endpoint.port })
|
||||
end
|
||||
end
|
||||
backend.endpoints = endpoints
|
||||
return backend
|
||||
end
|
||||
|
||||
local function sync_backend(backend)
|
||||
local implementation = get_implementation(backend)
|
||||
local balancer = balancers[backend.name]
|
||||
|
|
@ -59,6 +74,11 @@ local function sync_backend(backend)
|
|||
return
|
||||
end
|
||||
|
||||
local service_type = backend.service and backend.service.spec and backend.service.spec["type"]
|
||||
if service_type == "ExternalName" then
|
||||
backend = resolve_external_names(backend)
|
||||
end
|
||||
|
||||
balancer:sync(backend)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
local balancer_resty = require("balancer.resty")
|
||||
local resty_chash = require("resty.chash")
|
||||
local util = require("util")
|
||||
local split = require("util.split")
|
||||
|
||||
local _M = balancer_resty:new({ factory = resty_chash, name = "chash" })
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ end
|
|||
function _M.balance(self)
|
||||
local key = util.lua_ngx_var(self.hash_by)
|
||||
local endpoint_string = self.instance:find(key)
|
||||
return util.split_pair(endpoint_string, ":")
|
||||
return split.split_pair(endpoint_string, ":")
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
local resty_lock = require("resty.lock")
|
||||
local util = require("util")
|
||||
local split = require("util.split")
|
||||
|
||||
local DECAY_TIME = 10 -- this value is in seconds
|
||||
local LOCK_KEY = ":ewma_key"
|
||||
|
|
@ -131,10 +132,10 @@ function _M.balance(self)
|
|||
end
|
||||
|
||||
function _M.after_balance(_)
|
||||
local response_time = tonumber(util.get_first_value(ngx.var.upstream_response_time)) or 0
|
||||
local connect_time = tonumber(util.get_first_value(ngx.var.upstream_connect_time)) or 0
|
||||
local response_time = tonumber(split.get_first_value(ngx.var.upstream_response_time)) or 0
|
||||
local connect_time = tonumber(split.get_first_value(ngx.var.upstream_connect_time)) or 0
|
||||
local rtt = connect_time + response_time
|
||||
local upstream = util.get_first_value(ngx.var.upstream_addr)
|
||||
local upstream = split.get_first_value(ngx.var.upstream_addr)
|
||||
|
||||
if util.is_blank(upstream) then
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
local balancer_resty = require("balancer.resty")
|
||||
local resty_roundrobin = require("resty.roundrobin")
|
||||
local util = require("util")
|
||||
local split = require("util.split")
|
||||
|
||||
local _M = balancer_resty:new({ factory = resty_roundrobin, name = "round_robin" })
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ end
|
|||
|
||||
function _M.balance(self)
|
||||
local endpoint_string = self.instance:find()
|
||||
return util.split_pair(endpoint_string, ":")
|
||||
return split.split_pair(endpoint_string, ":")
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
local balancer_resty = require("balancer.resty")
|
||||
local resty_chash = require("resty.chash")
|
||||
local util = require("util")
|
||||
local split = require("util.split")
|
||||
local ck = require("resty.cookie")
|
||||
|
||||
local _M = balancer_resty:new({ factory = resty_chash, name = "sticky" })
|
||||
|
|
@ -74,7 +75,7 @@ end
|
|||
|
||||
function _M.balance(self)
|
||||
local endpoint_string = sticky_endpoint_string(self)
|
||||
return util.split_pair(endpoint_string, ":")
|
||||
return split.split_pair(endpoint_string, ":")
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
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 = {}
|
||||
local _M = {
|
||||
nameservers = {}
|
||||
}
|
||||
|
||||
function _M.get_backends_data()
|
||||
return configuration_data:get("backends")
|
||||
|
|
@ -27,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
|
||||
|
|
@ -34,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!")
|
||||
|
|
@ -63,4 +122,8 @@ function _M.call()
|
|||
ngx.status = ngx.HTTP_CREATED
|
||||
end
|
||||
|
||||
if _TEST then
|
||||
_M.handle_servers = handle_servers
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
|
|||
50
rootfs/etc/nginx/lua/monitor.lua
Normal file
50
rootfs/etc/nginx/lua/monitor.lua
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
local socket = ngx.socket.tcp
|
||||
local cjson = require('cjson')
|
||||
local defer = require('util.defer')
|
||||
local assert = assert
|
||||
|
||||
local _M = {}
|
||||
|
||||
local function send_data(jsonData)
|
||||
local s = assert(socket())
|
||||
assert(s:connect('unix:/tmp/prometheus-nginx.socket'))
|
||||
assert(s:send(jsonData))
|
||||
assert(s:close())
|
||||
end
|
||||
|
||||
function _M.encode_nginx_stats()
|
||||
return cjson.encode({
|
||||
host = ngx.var.host or "-",
|
||||
|
||||
method = ngx.var.request_method or "-",
|
||||
path = ngx.var.location_path or "-",
|
||||
|
||||
status = ngx.var.status or "-",
|
||||
|
||||
requestLength = tonumber(ngx.var.request_length) or -1,
|
||||
requestTime = tonumber(ngx.var.request_time) or -1,
|
||||
|
||||
responseLength = tonumber(ngx.var.bytes_sent) or -1,
|
||||
|
||||
endpoint = ngx.var.upstream_addr or "-",
|
||||
|
||||
upstreamLatency = tonumber(ngx.var.upstream_connect_time) or -1,
|
||||
upstreamResponseTime = tonumber(ngx.var.upstream_response_time) or -1,
|
||||
upstreamResponseLength = tonumber(ngx.var.upstream_response_length) or -1,
|
||||
upstreamStatus = ngx.var.upstream_status or "-",
|
||||
|
||||
namespace = ngx.var.namespace or "-",
|
||||
ingress = ngx.var.ingress_name or "-",
|
||||
service = ngx.var.service_name or "-",
|
||||
})
|
||||
end
|
||||
|
||||
function _M.call()
|
||||
local ok, err = defer.to_timer_phase(send_data, _M.encode_nginx_stats())
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "failed to defer send_data to timer phase: ", err)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
package.path = "./rootfs/etc/nginx/lua/?.lua;./rootfs/etc/nginx/lua/test/mocks/?.lua;" .. package.path
|
||||
|
||||
describe("Balancer chash", function()
|
||||
local balancer_chash = require("balancer.chash")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,5 @@
|
|||
package.path = "./rootfs/etc/nginx/lua/?.lua;./rootfs/etc/nginx/lua/test/mocks/?.lua;" .. package.path
|
||||
|
||||
local util = require("util")
|
||||
|
||||
local _ngx = {
|
||||
shared = {
|
||||
balancer_ewma = { flush_all = function() end },
|
||||
balancer_ewma_last_touched_at = { flush_all = function() end }
|
||||
},
|
||||
log = function(...) end,
|
||||
now = function() return os.time() end,
|
||||
}
|
||||
_G.ngx = _ngx
|
||||
|
||||
describe("Balancer ewma", function()
|
||||
local balancer_ewma = require("balancer.ewma")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
package.path = "./rootfs/etc/nginx/lua/?.lua;./rootfs/etc/nginx/lua/test/mocks/?.lua;" .. package.path
|
||||
_G._TEST = true
|
||||
|
||||
local _ngx = {
|
||||
shared = {},
|
||||
log = function(...) end,
|
||||
}
|
||||
_G.ngx = _ngx
|
||||
|
||||
local balancer, expected_implementations, backends
|
||||
|
||||
local function reset_balancer()
|
||||
|
|
@ -84,6 +77,44 @@ describe("Balancer", function()
|
|||
assert.spy(s).was_called_with(implementation, backend)
|
||||
end)
|
||||
|
||||
it("resolves external name to endpoints when service is of type External name", function()
|
||||
backend = {
|
||||
name = "exmaple-com", service = { spec = { ["type"] = "ExternalName" } },
|
||||
endpoints = {
|
||||
{ address = "example.com", port = "80", maxFails = 0, failTimeout = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
local dns_helper = require("test/dns_helper")
|
||||
dns_helper.mock_dns_query({
|
||||
{
|
||||
name = "example.com",
|
||||
address = "192.168.1.1",
|
||||
ttl = 3600,
|
||||
},
|
||||
{
|
||||
name = "example.com",
|
||||
address = "1.2.3.4",
|
||||
ttl = 60,
|
||||
}
|
||||
})
|
||||
expected_backend = {
|
||||
name = "exmaple-com", service = { spec = { ["type"] = "ExternalName" } },
|
||||
endpoints = {
|
||||
{ address = "192.168.1.1", port = "80" },
|
||||
{ address = "1.2.3.4", port = "80" },
|
||||
}
|
||||
}
|
||||
|
||||
local mock_instance = { sync = function(backend) end }
|
||||
setmetatable(mock_instance, implementation)
|
||||
implementation.new = function(self, backend) return mock_instance end
|
||||
assert.has_no.errors(function() balancer.sync_backend(backend) end)
|
||||
stub(mock_instance, "sync")
|
||||
assert.has_no.errors(function() balancer.sync_backend(backend) end)
|
||||
assert.stub(mock_instance.sync).was_called_with(mock_instance, expected_backend)
|
||||
end)
|
||||
|
||||
it("replaces the existing balancer when load balancing config changes for backend", function()
|
||||
assert.has_no.errors(function() balancer.sync_backend(backend) end)
|
||||
|
||||
|
|
@ -92,10 +123,10 @@ describe("Balancer", function()
|
|||
|
||||
local s_old = spy.on(implementation, "new")
|
||||
local s = spy.on(new_implementation, "new")
|
||||
local s_ngx_log = spy.on(_G.ngx, "log")
|
||||
local s_ngx_log = spy.on(ngx, "log")
|
||||
|
||||
assert.has_no.errors(function() balancer.sync_backend(backend) end)
|
||||
assert.spy(s_ngx_log).was_called_with(ngx.ERR,
|
||||
assert.spy(s_ngx_log).was_called_with(ngx.INFO,
|
||||
"LB algorithm changed from round_robin to ewma, resetting the instance")
|
||||
-- TODO(elvinefendi) figure out why
|
||||
-- assert.spy(s).was_called_with(new_implementation, backend) does not work here
|
||||
|
|
|
|||
262
rootfs/etc/nginx/lua/test/configuration_test.lua
Normal file
262
rootfs/etc/nginx/lua/test/configuration_test.lua
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
_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_backends()
|
||||
return {
|
||||
{
|
||||
name = "my-dummy-backend-1", ["load-balance"] = "sticky",
|
||||
endpoints = { { address = "10.183.7.40", port = "8080", maxFails = 0, failTimeout = 0 } },
|
||||
sessionAffinityConfig = { name = "cookie", cookieSessionAffinity = { name = "route", hash = "sha1" } },
|
||||
},
|
||||
{
|
||||
name = "my-dummy-backend-2", ["load-balance"] = "ewma",
|
||||
endpoints = {
|
||||
{ address = "10.184.7.40", port = "7070", maxFails = 3, failTimeout = 2 },
|
||||
{ address = "10.184.7.41", port = "7070", maxFails = 2, failTimeout = 1 },
|
||||
}
|
||||
},
|
||||
{
|
||||
name = "my-dummy-backend-3", ["load-balance"] = "round_robin",
|
||||
endpoints = {
|
||||
{ address = "10.185.7.40", port = "6060", maxFails = 0, failTimeout = 0 },
|
||||
{ address = "10.185.7.41", port = "6060", maxFails = 2, failTimeout = 1 },
|
||||
}
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function get_mocked_ngx_env()
|
||||
local _ngx = {
|
||||
status = ngx.HTTP_OK,
|
||||
var = {},
|
||||
req = {
|
||||
read_body = function() end,
|
||||
get_body_data = function() return cjson.encode(get_backends()) end,
|
||||
get_body_file = function() return nil end,
|
||||
},
|
||||
log = function(msg) end,
|
||||
}
|
||||
setmetatable(_ngx, {__index = _G.ngx})
|
||||
return _ngx
|
||||
end
|
||||
|
||||
describe("Configuration", function()
|
||||
before_each(function()
|
||||
_G.ngx = get_mocked_ngx_env()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
_G.ngx = unmocked_ngx
|
||||
package.loaded["configuration"] = nil
|
||||
configuration = require("configuration")
|
||||
end)
|
||||
|
||||
describe("Backends", function()
|
||||
context("Request method is neither GET nor POST", function()
|
||||
it("sends 'Only POST and GET requests are allowed!' in the response body", function()
|
||||
ngx.var.request_method = "PUT"
|
||||
local s = spy.on(ngx, "print")
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.spy(s).was_called_with("Only POST and GET requests are allowed!")
|
||||
end)
|
||||
|
||||
it("returns a status code of 400", function()
|
||||
ngx.var.request_method = "PUT"
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.equal(ngx.status, ngx.HTTP_BAD_REQUEST)
|
||||
end)
|
||||
end)
|
||||
|
||||
context("GET request to /configuration/backends", function()
|
||||
before_each(function()
|
||||
ngx.var.request_method = "GET"
|
||||
ngx.var.request_uri = "/configuration/backends"
|
||||
end)
|
||||
|
||||
it("returns the current configured backends on the response body", function()
|
||||
-- Encoding backends since comparing tables fail due to reference comparison
|
||||
local encoded_backends = cjson.encode(get_backends())
|
||||
ngx.shared.configuration_data:set("backends", encoded_backends)
|
||||
local s = spy.on(ngx, "print")
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.spy(s).was_called_with(encoded_backends)
|
||||
end)
|
||||
|
||||
it("returns a status of 200", function()
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.equal(ngx.status, ngx.HTTP_OK)
|
||||
end)
|
||||
end)
|
||||
|
||||
context("POST request to /configuration/backends", function()
|
||||
before_each(function()
|
||||
ngx.var.request_method = "POST"
|
||||
ngx.var.request_uri = "/configuration/backends"
|
||||
end)
|
||||
|
||||
it("stores the posted backends on the shared dictionary", function()
|
||||
-- Encoding backends since comparing tables fail due to reference comparison
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.equal(ngx.shared.configuration_data:get("backends"), cjson.encode(get_backends()))
|
||||
end)
|
||||
|
||||
context("Failed to read request body", function()
|
||||
local mocked_get_body_data = ngx.req.get_body_data
|
||||
before_each(function()
|
||||
ngx.req.get_body_data = function() return nil end
|
||||
end)
|
||||
|
||||
teardown(function()
|
||||
ngx.req.get_body_data = mocked_get_body_data
|
||||
end)
|
||||
|
||||
it("returns a status of 400", function()
|
||||
_G.io.open = function(filename, extension) return false end
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.equal(ngx.status, ngx.HTTP_BAD_REQUEST)
|
||||
end)
|
||||
|
||||
it("logs 'dynamic-configuration: unable to read valid request body to stderr'", function()
|
||||
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")
|
||||
end)
|
||||
end)
|
||||
|
||||
context("Failed to set the new backends to the configuration dictionary", function()
|
||||
local resty_configuration_data_set = ngx.shared.configuration_data.set
|
||||
before_each(function()
|
||||
ngx.shared.configuration_data.set = function(key, value) return false, "" end
|
||||
end)
|
||||
|
||||
teardown(function()
|
||||
ngx.shared.configuration_data.set = resty_configuration_data_set
|
||||
end)
|
||||
|
||||
it("returns a status of 400", function()
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.equal(ngx.status, ngx.HTTP_BAD_REQUEST)
|
||||
end)
|
||||
|
||||
it("logs 'dynamic-configuration: error updating configuration:' to stderr", function()
|
||||
local s = spy.on(ngx, "log")
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.spy(s).was_called_with(ngx.ERR, "dynamic-configuration: error updating configuration: ")
|
||||
end)
|
||||
end)
|
||||
|
||||
context("Succeeded to update backends configuration", function()
|
||||
it("returns a status of 201", function()
|
||||
assert.has_no.errors(configuration.call)
|
||||
assert.equal(ngx.status, ngx.HTTP_CREATED)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
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)
|
||||
12
rootfs/etc/nginx/lua/test/defer_test.lua
Normal file
12
rootfs/etc/nginx/lua/test/defer_test.lua
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
_G._TEST = true
|
||||
local defer = require("util.defer")
|
||||
|
||||
describe("Defer", function()
|
||||
describe("to_timer_phase", function()
|
||||
it("executes passed callback immediately if called on timer phase", function()
|
||||
defer.counter = 0
|
||||
defer.to_timer_phase(function() defer.counter = defer.counter + 1 end)
|
||||
assert.equal(defer.counter, 1)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
27
rootfs/etc/nginx/lua/test/dns_helper.lua
Normal file
27
rootfs/etc/nginx/lua/test/dns_helper.lua
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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
|
||||
|
|
@ -1 +0,0 @@
|
|||
return {}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
return {
|
||||
new = function(self, nodes) return {} end
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
return {}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
return {
|
||||
new = function(self, name, ...) return {} end,
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
return {}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
return {
|
||||
new = function(self, nodes) return {} end
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
return {}
|
||||
|
|
@ -1 +0,0 @@
|
|||
return {}
|
||||
96
rootfs/etc/nginx/lua/test/monitor_test.lua
Normal file
96
rootfs/etc/nginx/lua/test/monitor_test.lua
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
_G._TEST = true
|
||||
local cjson = require("cjson")
|
||||
|
||||
describe("Monitor", function()
|
||||
local monitor = require("monitor")
|
||||
describe("encode_nginx_stats()", function()
|
||||
it("successfuly encodes the current stats of nginx to JSON", function()
|
||||
local nginx_environment = {
|
||||
host = "testshop.com",
|
||||
status = "200",
|
||||
bytes_sent = "150",
|
||||
server_protocol = "HTTP",
|
||||
request_method = "GET",
|
||||
location_path = "/admin",
|
||||
request_length = "300",
|
||||
request_time = "210",
|
||||
proxy_upstream_name = "test-upstream",
|
||||
upstream_addr = "2.2.2.2",
|
||||
upstream_response_time = "200",
|
||||
upstream_response_length = "150",
|
||||
upstream_connect_time = "1",
|
||||
upstream_status = "220",
|
||||
namespace = "test-app-production",
|
||||
ingress_name = "web-yml",
|
||||
service_name = "test-app",
|
||||
}
|
||||
ngx.var = nginx_environment
|
||||
|
||||
local encode_nginx_stats = monitor.encode_nginx_stats
|
||||
local encoded_json_stats = encode_nginx_stats()
|
||||
local decoded_json_stats = cjson.decode(encoded_json_stats)
|
||||
|
||||
local expected_json_stats = {
|
||||
host = "testshop.com",
|
||||
status = "200",
|
||||
responseLength = 150.0,
|
||||
method = "GET",
|
||||
path = "/admin",
|
||||
requestLength = 300.0,
|
||||
requestTime = 210.0,
|
||||
endpoint = "2.2.2.2",
|
||||
upstreamResponseTime = 200,
|
||||
upstreamStatus = "220",
|
||||
upstreamLatency = 1.0,
|
||||
upstreamResponseLength = 150.0,
|
||||
namespace = "test-app-production",
|
||||
ingress = "web-yml",
|
||||
service = "test-app",
|
||||
}
|
||||
|
||||
assert.are.same(decoded_json_stats,expected_json_stats)
|
||||
end)
|
||||
|
||||
it("replaces empty numeric keys with -1 and missing string keys with -", function()
|
||||
local nginx_environment = {
|
||||
remote_addr = "10.10.10.10",
|
||||
realip_remote_addr = "5.5.5.5",
|
||||
remote_user = "francisco",
|
||||
server_protocol = "HTTP",
|
||||
request_method = "GET",
|
||||
location_path = "/admin",
|
||||
request_time = "202",
|
||||
proxy_upstream_name = "test-upstream",
|
||||
upstream_addr = "2.2.2.2",
|
||||
upstream_response_time = "201",
|
||||
upstream_status = "220",
|
||||
ingress_name = "web-yml",
|
||||
}
|
||||
ngx.var = nginx_environment
|
||||
|
||||
local encode_nginx_stats = monitor.encode_nginx_stats
|
||||
local encoded_json_stats = encode_nginx_stats()
|
||||
local decoded_json_stats = cjson.decode(encoded_json_stats)
|
||||
|
||||
local expected_json_stats = {
|
||||
host = "-",
|
||||
status = "-",
|
||||
responseLength = -1,
|
||||
method = "GET",
|
||||
path = "/admin",
|
||||
requestLength = -1,
|
||||
requestTime = 202.0,
|
||||
endpoint = "2.2.2.2",
|
||||
upstreamStatus = "220",
|
||||
namespace = "-",
|
||||
ingress = "web-yml",
|
||||
upstreamLatency = -1,
|
||||
upstreamResponseTime = 201,
|
||||
upstreamResponseLength = -1,
|
||||
responseLength = -1,
|
||||
service = "-",
|
||||
}
|
||||
assert.are.same(decoded_json_stats,expected_json_stats)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
32
rootfs/etc/nginx/lua/test/run.lua
Normal file
32
rootfs/etc/nginx/lua/test/run.lua
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
local ffi = require("ffi")
|
||||
|
||||
-- without this we get errors such as "attempt to redefine XXX"
|
||||
local old_cdef = ffi.cdef
|
||||
local exists = {}
|
||||
ffi.cdef = function(def)
|
||||
if exists[def] then
|
||||
return
|
||||
end
|
||||
exists[def] = true
|
||||
return old_cdef(def)
|
||||
end
|
||||
|
||||
local old_udp = ngx.socket.udp
|
||||
ngx.socket.udp = function(...)
|
||||
local socket = old_udp(...)
|
||||
socket.send = function(...)
|
||||
error("ngx.socket.udp:send please mock this to use in tests")
|
||||
end
|
||||
return socket
|
||||
end
|
||||
|
||||
local old_tcp = ngx.socket.tcp
|
||||
ngx.socket.tcp = function(...)
|
||||
local socket = old_tcp(...)
|
||||
socket.send = function(...)
|
||||
error("ngx.socket.tcp:send please mock this to use in tests")
|
||||
end
|
||||
return socket
|
||||
end
|
||||
|
||||
require "busted.runner"({ standalone = false })
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2018 The Kubernetes 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.
|
||||
|
||||
install()
|
||||
{
|
||||
package="$1"
|
||||
version="$2"
|
||||
|
||||
if luarocks list --porcelain $package $version | grep -q "installed"; then
|
||||
echo $package already installed, skipping ;
|
||||
else
|
||||
sudo luarocks install $package $version;
|
||||
fi
|
||||
}
|
||||
|
||||
install busted 2.0.rc12
|
||||
install lua-cjson 2.1.0-1
|
||||
74
rootfs/etc/nginx/lua/test/util/dns_test.lua
Normal file
74
rootfs/etc/nginx/lua/test/util/dns_test.lua
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
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)
|
||||
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" })
|
||||
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)
|
||||
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!")
|
||||
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: oops!")
|
||||
|
||||
dns_helper.mock_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, "server returned error code: 1: format error")
|
||||
|
||||
dns_helper.mock_dns_query({})
|
||||
assert.are.same({ "example.com" }, dns.resolve("example.com"))
|
||||
assert.spy(s_ngx_log).was_called_with(ngx.ERR, "no A record resolved")
|
||||
|
||||
dns_helper.mock_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, "no A 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({
|
||||
{
|
||||
name = "example.com",
|
||||
address = "192.168.1.1",
|
||||
ttl = 3600,
|
||||
},
|
||||
{
|
||||
name = "example.com",
|
||||
address = "1.2.3.4",
|
||||
ttl = 60,
|
||||
}
|
||||
})
|
||||
|
||||
local lrucache = require("resty.lrucache")
|
||||
local old_lrucache_new = lrucache.new
|
||||
lrucache.new = function(...)
|
||||
local cache = old_lrucache_new(...)
|
||||
|
||||
local old_set = cache.set
|
||||
cache.set = function(self, key, value, ttl)
|
||||
assert.equal("example.com", key)
|
||||
assert.are.same({ "192.168.1.1", "1.2.3.4" }, value)
|
||||
assert.equal(60, ttl)
|
||||
return old_set(self, key, value, ttl)
|
||||
end
|
||||
|
||||
return cache
|
||||
end
|
||||
|
||||
assert.are.same({ "192.168.1.1", "1.2.3.4" }, dns.resolve("example.com"))
|
||||
|
||||
dns_helper.mock_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"))
|
||||
end)
|
||||
end)
|
||||
|
|
@ -49,17 +49,6 @@ function _M.lua_ngx_var(ngx_var)
|
|||
return ngx.var[var_name]
|
||||
end
|
||||
|
||||
function _M.split_pair(pair, seperator)
|
||||
local i = pair:find(seperator)
|
||||
if i == nil then
|
||||
return pair, nil
|
||||
else
|
||||
local name = pair:sub(1, i - 1)
|
||||
local value = pair:sub(i + 1, -1)
|
||||
return name, value
|
||||
end
|
||||
end
|
||||
|
||||
-- this implementation is taken from
|
||||
-- https://web.archive.org/web/20131225070434/http://snippets.luacode.org/snippets/Deep_Comparison_of_Two_Values_3
|
||||
-- and modified for use in this project
|
||||
|
|
@ -88,30 +77,6 @@ function _M.is_blank(str)
|
|||
return str == nil or string_len(str) == 0
|
||||
end
|
||||
|
||||
-- http://nginx.org/en/docs/http/ngx_http_upstream_module.html#example
|
||||
-- CAVEAT: nginx is giving out : instead of , so the docs are wrong
|
||||
-- 127.0.0.1:26157 : 127.0.0.1:26157 , ngx.var.upstream_addr
|
||||
-- 200 : 200 , ngx.var.upstream_status
|
||||
-- 0.00 : 0.00, ngx.var.upstream_response_time
|
||||
function _M.split_upstream_var(var)
|
||||
if not var then
|
||||
return nil, nil
|
||||
end
|
||||
local t = {}
|
||||
for v in var:gmatch("[^%s|,]+") do
|
||||
if v ~= ":" then
|
||||
t[#t+1] = v
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function _M.get_first_value(var)
|
||||
local t = _M.split_upstream_var(var) or {}
|
||||
if #t == 0 then return nil end
|
||||
return t[1]
|
||||
end
|
||||
|
||||
-- this implementation is taken from:
|
||||
-- https://github.com/luafun/luafun/blob/master/fun.lua#L33
|
||||
-- SHA: 04c99f9c393e54a604adde4b25b794f48104e0d0
|
||||
|
|
@ -130,4 +95,13 @@ local function deepcopy(orig)
|
|||
end
|
||||
_M.deepcopy = deepcopy
|
||||
|
||||
local function tablelength(T)
|
||||
local count = 0
|
||||
for _ in pairs(T) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
_M.tablelength = tablelength
|
||||
|
||||
return _M
|
||||
|
|
|
|||
57
rootfs/etc/nginx/lua/util/defer.lua
Normal file
57
rootfs/etc/nginx/lua/util/defer.lua
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
local util = require("util")
|
||||
|
||||
local timer_started = false
|
||||
local queue = {}
|
||||
local MAX_QUEUE_SIZE = 10000
|
||||
|
||||
local _M = {}
|
||||
|
||||
local function flush_queue(premature)
|
||||
-- TODO Investigate if we should actually still flush the queue when we're
|
||||
-- shutting down.
|
||||
if premature then return end
|
||||
|
||||
local current_queue = queue
|
||||
queue = {}
|
||||
timer_started = false
|
||||
|
||||
for _,v in ipairs(current_queue) do
|
||||
v.func(unpack(v.args))
|
||||
end
|
||||
end
|
||||
|
||||
-- `to_timer_phase` will enqueue a function that will be executed in a timer
|
||||
-- context, at a later point in time. The purpose is that some APIs (such as
|
||||
-- sockets) are not available during some nginx request phases (such as the
|
||||
-- logging phase), but are available for use in timers. There are no ordering
|
||||
-- guarantees for when a function will be executed.
|
||||
function _M.to_timer_phase(func, ...)
|
||||
if ngx.get_phase() == "timer" then
|
||||
func(...)
|
||||
return true
|
||||
end
|
||||
|
||||
if #queue >= MAX_QUEUE_SIZE then
|
||||
ngx.log(ngx.ERR, "deferred timer queue full")
|
||||
return nil, "deferred timer queue full"
|
||||
end
|
||||
|
||||
table.insert(queue, { func = func, args = {...} })
|
||||
if not timer_started then
|
||||
local ok, err = ngx.timer.at(0, flush_queue)
|
||||
if ok then
|
||||
-- unfortunately this is to deal with tests - when running unit tests, we
|
||||
-- dont actually run the timer, we call the function inline
|
||||
if util.tablelength(queue) > 0 then
|
||||
timer_started = true
|
||||
end
|
||||
else
|
||||
local msg = "failed to create timer: " .. tostring(err)
|
||||
ngx.log(ngx.ERR, msg)
|
||||
return nil, msg
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return _M
|
||||
75
rootfs/etc/nginx/lua/util/dns.lua
Normal file
75
rootfs/etc/nginx/lua/util/dns.lua
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
local resolver = require("resty.dns.resolver")
|
||||
local lrucache = require("resty.lrucache")
|
||||
local configuration = require("configuration")
|
||||
local util = require("util")
|
||||
|
||||
local _M = {}
|
||||
local CACHE_SIZE = 10000
|
||||
local MAXIMUM_TTL_VALUE = 2147483647 -- maximum value according to https://tools.ietf.org/html/rfc2181
|
||||
|
||||
local cache, err = lrucache.new(CACHE_SIZE)
|
||||
if not cache then
|
||||
return error("failed to create the cache: " .. (err or "unknown"))
|
||||
end
|
||||
|
||||
local function a_records_and_max_ttl(answers)
|
||||
local addresses = {}
|
||||
local ttl = MAXIMUM_TTL_VALUE -- maximum value according to https://tools.ietf.org/html/rfc2181
|
||||
|
||||
for _, ans in ipairs(answers) do
|
||||
if ans.address then
|
||||
table.insert(addresses, ans.address)
|
||||
if ttl > ans.ttl then
|
||||
ttl = ans.ttl
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return addresses, ttl
|
||||
end
|
||||
|
||||
function _M.resolve(host)
|
||||
local cached_addresses = cache:get(host)
|
||||
if cached_addresses then
|
||||
local message = string.format(
|
||||
"addresses %s for host %s was resolved from cache",
|
||||
table.concat(cached_addresses, ", "), host)
|
||||
ngx.log(ngx.INFO, message)
|
||||
return cached_addresses
|
||||
end
|
||||
|
||||
local r
|
||||
r, err = resolver:new{
|
||||
nameservers = util.deepcopy(configuration.nameservers),
|
||||
retrans = 5,
|
||||
timeout = 2000, -- 2 sec
|
||||
}
|
||||
|
||||
if not r then
|
||||
ngx.log(ngx.ERR, "failed to instantiate the resolver: " .. tostring(err))
|
||||
return { host }
|
||||
end
|
||||
|
||||
local answers
|
||||
answers, err = r:query(host, { qtype = r.TYPE_A }, {})
|
||||
if not answers then
|
||||
ngx.log(ngx.ERR, "failed to query the DNS server: " .. tostring(err))
|
||||
return { host }
|
||||
end
|
||||
|
||||
if answers.errcode then
|
||||
ngx.log(ngx.ERR, string.format("server returned error code: %s: %s", answers.errcode, answers.errstr))
|
||||
return { host }
|
||||
end
|
||||
|
||||
local addresses, ttl = a_records_and_max_ttl(answers)
|
||||
if #addresses == 0 then
|
||||
ngx.log(ngx.ERR, "no A record resolved")
|
||||
return { host }
|
||||
end
|
||||
|
||||
cache:set(host, addresses, ttl)
|
||||
return addresses
|
||||
end
|
||||
|
||||
return _M
|
||||
70
rootfs/etc/nginx/lua/util/split.lua
Normal file
70
rootfs/etc/nginx/lua/util/split.lua
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
local _M = {}
|
||||
|
||||
-- splits strings into host and port
|
||||
local function parse_addr(addr)
|
||||
local _, _, host, port = addr:find("([^:]+):([^:]+)")
|
||||
if host and port then
|
||||
return {host=host, port=port}
|
||||
else
|
||||
return nil, "error in parsing upstream address!"
|
||||
end
|
||||
end
|
||||
|
||||
function _M.get_first_value(var)
|
||||
local t = _M.split_upstream_var(var) or {}
|
||||
if #t == 0 then return nil end
|
||||
return t[1]
|
||||
end
|
||||
|
||||
function _M.split_pair(pair, seperator)
|
||||
local i = pair:find(seperator)
|
||||
if i == nil then
|
||||
return pair, nil
|
||||
else
|
||||
local name = pair:sub(1, i - 1)
|
||||
local value = pair:sub(i + 1, -1)
|
||||
return name, value
|
||||
end
|
||||
end
|
||||
|
||||
-- http://nginx.org/en/docs/http/ngx_http_upstream_module.html#example
|
||||
-- CAVEAT: nginx is giving out : instead of , so the docs are wrong
|
||||
-- 127.0.0.1:26157 : 127.0.0.1:26157 , ngx.var.upstream_addr
|
||||
-- 200 : 200 , ngx.var.upstream_status
|
||||
-- 0.00 : 0.00, ngx.var.upstream_response_time
|
||||
function _M.split_upstream_var(var)
|
||||
if not var then
|
||||
return nil, nil
|
||||
end
|
||||
local t = {}
|
||||
for v in var:gmatch("[^%s|,]+") do
|
||||
if v ~= ":" then
|
||||
t[#t+1] = v
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Splits an NGINX $upstream_addr and returns an array of tables with a `host` and `port` key-value pair.
|
||||
function _M.split_upstream_addr(addrs_str)
|
||||
if not addrs_str then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local addrs = _M.split_upstream_var(addrs_str)
|
||||
local host_and_ports = {}
|
||||
|
||||
for _, v in ipairs(addrs) do
|
||||
local a, err = parse_addr(v)
|
||||
if err then
|
||||
return nil, err
|
||||
end
|
||||
host_and_ports[#host_and_ports+1] = a
|
||||
end
|
||||
if #host_and_ports == 0 then
|
||||
return nil, "no upstream addresses to parse!"
|
||||
end
|
||||
return host_and_ports
|
||||
end
|
||||
|
||||
return _M
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# A very simple nginx configuration file that forces nginx to start.
|
||||
pid /run/nginx.pid;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {}
|
||||
http {}
|
||||
|
|
|
|||
1
rootfs/etc/nginx/opentracing.json
Normal file
1
rootfs/etc/nginx/opentracing.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -7,11 +7,18 @@
|
|||
{{ $proxyHeaders := .ProxySetHeaders }}
|
||||
{{ $addHeaders := .AddHeaders }}
|
||||
|
||||
# Configuration checksum: {{ $all.Cfg.Checksum }}
|
||||
|
||||
# setup custom paths that do not require root access
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
{{ if $cfg.EnableModsecurity }}
|
||||
load_module /etc/nginx/modules/ngx_http_modsecurity_module.so;
|
||||
{{ end }}
|
||||
|
||||
{{ buildOpentracingLoad $cfg }}
|
||||
{{ if $cfg.EnableOpentracing }}
|
||||
load_module /etc/nginx/modules/ngx_http_opentracing_module.so;
|
||||
{{ end }}
|
||||
|
||||
daemon off;
|
||||
|
||||
|
|
@ -20,7 +27,6 @@ worker_processes {{ $cfg.WorkerProcesses }};
|
|||
worker_cpu_affinity {{ $cfg.WorkerCpuAffinity }};
|
||||
{{ end }}
|
||||
|
||||
pid /run/nginx.pid;
|
||||
{{ if ne .MaxOpenFiles 0 }}
|
||||
worker_rlimit_nofile {{ .MaxOpenFiles }};
|
||||
{{ end }}
|
||||
|
|
@ -30,11 +36,15 @@ worker_rlimit_nofile {{ .MaxOpenFiles }};
|
|||
worker_shutdown_timeout {{ $cfg.WorkerShutdownTimeout }} ;
|
||||
|
||||
events {
|
||||
multi_accept on;
|
||||
multi_accept {{ if $cfg.EnableMultiAccept }}on{{ else }}off{{ end }};
|
||||
worker_connections {{ $cfg.MaxWorkerConnections }};
|
||||
use epoll;
|
||||
}
|
||||
|
||||
{{ if not (empty $cfg.MainSnippet) }}
|
||||
{{ $cfg.MainSnippet }}
|
||||
{{ end }}
|
||||
|
||||
http {
|
||||
{{ if not $all.DisableLua }}
|
||||
lua_package_cpath "/usr/local/lib/lua/?.so;/usr/lib/lua-platform-path/lua/5.1/?.so;;";
|
||||
|
|
@ -58,6 +68,7 @@ http {
|
|||
error("require failed: " .. tostring(res))
|
||||
else
|
||||
configuration = res
|
||||
configuration.nameservers = { {{ buildResolversForLua $cfg.Resolver $cfg.DisableIpv6DNS }} }
|
||||
end
|
||||
|
||||
ok, res = pcall(require, "balancer")
|
||||
|
|
@ -67,6 +78,13 @@ http {
|
|||
balancer = res
|
||||
end
|
||||
{{ end }}
|
||||
|
||||
ok, res = pcall(require, "monitor")
|
||||
if not ok then
|
||||
error("require failed: " .. tostring(res))
|
||||
else
|
||||
monitor = res
|
||||
end
|
||||
}
|
||||
|
||||
{{ if $all.DynamicConfigurationEnabled }}
|
||||
|
|
@ -101,11 +119,6 @@ http {
|
|||
geoip_proxy_recursive on;
|
||||
{{ end }}
|
||||
|
||||
{{ if $cfg.EnableVtsStatus }}
|
||||
vhost_traffic_status_zone shared:vhost_traffic_status:{{ $cfg.VtsStatusZoneSize }};
|
||||
vhost_traffic_status_filter_by_set_key {{ $cfg.VtsDefaultFilterKey }};
|
||||
{{ end }}
|
||||
|
||||
aio threads;
|
||||
aio_write on;
|
||||
|
||||
|
|
@ -119,6 +132,11 @@ http {
|
|||
keepalive_timeout {{ $cfg.KeepAlive }}s;
|
||||
keepalive_requests {{ $cfg.KeepAliveRequests }};
|
||||
|
||||
client_body_temp_path /tmp/client-body;
|
||||
fastcgi_temp_path /tmp/fastcgi-temp;
|
||||
proxy_temp_path /tmp/proxy-temp;
|
||||
ajp_temp_path /tmp/ajp-temp;
|
||||
|
||||
client_header_buffer_size {{ $cfg.ClientHeaderBufferSize }};
|
||||
client_header_timeout {{ $cfg.ClientHeaderTimeout }}s;
|
||||
large_client_header_buffers {{ $cfg.LargeClientHeaderBuffers }};
|
||||
|
|
@ -161,7 +179,7 @@ http {
|
|||
|
||||
{{ if $cfg.UseGzip }}
|
||||
gzip on;
|
||||
gzip_comp_level 5;
|
||||
gzip_comp_level {{ $cfg.GzipLevel }};
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types {{ $cfg.GzipTypes }};
|
||||
|
|
@ -176,7 +194,7 @@ http {
|
|||
|
||||
server_tokens {{ if $cfg.ShowServerTokens }}on{{ else }}off{{ end }};
|
||||
{{ if not $cfg.ShowServerTokens }}
|
||||
more_set_headers "Server: ";
|
||||
more_clear_headers Server;
|
||||
{{ end }}
|
||||
|
||||
# disable warnings
|
||||
|
|
@ -186,6 +204,7 @@ http {
|
|||
# $namespace
|
||||
# $ingress_name
|
||||
# $service_name
|
||||
# $service_port
|
||||
log_format upstreaminfo {{ if $cfg.LogFormatEscapeJSON }}escape=json {{ end }}'{{ buildLogFormatUpstream $cfg }}';
|
||||
|
||||
{{/* map urls that should not appear in access.log */}}
|
||||
|
|
@ -379,10 +398,10 @@ http {
|
|||
{{ end }}
|
||||
|
||||
{{ if not $all.DynamicConfigurationEnabled }}
|
||||
{{ range $name, $upstream := $backends }}
|
||||
{{ range $upstream := $backends }}
|
||||
{{ if eq $upstream.SessionAffinity.AffinityType "cookie" }}
|
||||
upstream sticky-{{ $upstream.Name }} {
|
||||
sticky hash={{ $upstream.SessionAffinity.CookieSessionAffinity.Hash }} name={{ $upstream.SessionAffinity.CookieSessionAffinity.Name }} httponly;
|
||||
sticky hash={{ $upstream.SessionAffinity.CookieSessionAffinity.Hash }} name={{ $upstream.SessionAffinity.CookieSessionAffinity.Name }}{{if eq (len $upstream.SessionAffinity.CookieSessionAffinity.Locations) 1 }}{{ range $locationName, $locationPaths := $upstream.SessionAffinity.CookieSessionAffinity.Locations }}{{ if eq (len $locationPaths) 1 }} path={{ index $locationPaths 0 }}{{ end }}{{ end }}{{ end }} httponly;
|
||||
|
||||
{{ if (gt $cfg.UpstreamKeepaliveConnections 0) }}
|
||||
keepalive {{ $cfg.UpstreamKeepaliveConnections }};
|
||||
|
|
@ -421,7 +440,7 @@ http {
|
|||
{{ end }}
|
||||
|
||||
{{/* build the maps that will be use to validate the Whitelist */}}
|
||||
{{ range $index, $server := $servers }}
|
||||
{{ range $server := $servers }}
|
||||
{{ range $location := $server.Locations }}
|
||||
{{ $path := buildLocation $location }}
|
||||
|
||||
|
|
@ -491,7 +510,7 @@ http {
|
|||
}
|
||||
{{ end }}
|
||||
|
||||
{{ range $index, $server := $servers }}
|
||||
{{ range $server := $servers }}
|
||||
|
||||
## start server {{ $server.Hostname }}
|
||||
server {
|
||||
|
|
@ -511,9 +530,6 @@ http {
|
|||
|
||||
# default server, used for NGINX healthcheck and access to nginx stats
|
||||
server {
|
||||
# Use the port {{ $all.ListenPorts.Status }} (random value just to avoid known ports) as default port for nginx.
|
||||
# Changing this value requires a change in:
|
||||
# https://github.com/kubernetes/ingress-nginx/blob/master/controllers/nginx/pkg/cmd/controller/nginx.go
|
||||
listen {{ $all.ListenPorts.Status }} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }};
|
||||
{{ if $IsIPV6Enabled }}listen [::]:{{ $all.ListenPorts.Status }} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }};{{ end }}
|
||||
set $proxy_upstream_name "-";
|
||||
|
|
@ -551,14 +567,8 @@ http {
|
|||
opentracing off;
|
||||
{{ end }}
|
||||
|
||||
{{ if $cfg.EnableVtsStatus }}
|
||||
vhost_traffic_status_display;
|
||||
vhost_traffic_status_display_format html;
|
||||
vhost_traffic_status_display_sum_key {{ $cfg.VtsSumKey }};
|
||||
{{ else }}
|
||||
access_log off;
|
||||
stub_status on;
|
||||
{{ end }}
|
||||
}
|
||||
|
||||
{{ if $all.DynamicConfigurationEnabled }}
|
||||
|
|
@ -612,10 +622,10 @@ stream {
|
|||
error_log {{ $cfg.ErrorLogPath }};
|
||||
|
||||
# TCP services
|
||||
{{ range $i, $tcpServer := .TCPBackends }}
|
||||
{{ range $tcpServer := .TCPBackends }}
|
||||
upstream tcp-{{ $tcpServer.Port }}-{{ $tcpServer.Backend.Namespace }}-{{ $tcpServer.Backend.Name }}-{{ $tcpServer.Backend.Port }} {
|
||||
{{ range $j, $endpoint := $tcpServer.Endpoints }}
|
||||
server {{ $endpoint.Address }}:{{ $endpoint.Port }};
|
||||
{{ range $endpoint := $tcpServer.Endpoints }}
|
||||
server {{ $endpoint.Address | formatIP }}:{{ $endpoint.Port }};
|
||||
{{ end }}
|
||||
}
|
||||
server {
|
||||
|
|
@ -641,10 +651,10 @@ stream {
|
|||
{{ end }}
|
||||
|
||||
# UDP services
|
||||
{{ range $i, $udpServer := .UDPBackends }}
|
||||
{{ range $udpServer := .UDPBackends }}
|
||||
upstream udp-{{ $udpServer.Port }}-{{ $udpServer.Backend.Namespace }}-{{ $udpServer.Backend.Name }}-{{ $udpServer.Backend.Port }} {
|
||||
{{ range $j, $endpoint := $udpServer.Endpoints }}
|
||||
server {{ $endpoint.Address }}:{{ $endpoint.Port }};
|
||||
{{ range $endpoint := $udpServer.Endpoints }}
|
||||
server {{ $endpoint.Address | formatIP }}:{{ $endpoint.Port }};
|
||||
{{ end }}
|
||||
}
|
||||
|
||||
|
|
@ -685,6 +695,9 @@ stream {
|
|||
proxy_set_header X-Namespace $namespace;
|
||||
proxy_set_header X-Ingress-Name $ingress_name;
|
||||
proxy_set_header X-Service-Name $service_name;
|
||||
proxy_set_header X-Service-Port $service_port;
|
||||
|
||||
set $proxy_upstream_name "upstream-default-backend";
|
||||
|
||||
rewrite (.*) / break;
|
||||
|
||||
|
|
@ -739,7 +752,7 @@ stream {
|
|||
|
||||
{{/* Listen on {{ $all.ListenPorts.SSLProxy }} because port {{ $all.ListenPorts.HTTPS }} is used in the TLS sni server */}}
|
||||
{{/* This listener must always have proxy_protocol enabled, because the SNI listener forwards on source IP info in it. */}}
|
||||
{{ if not (empty $server.SSLCertificate) }}
|
||||
{{ if not (empty $server.SSLCert.PemFileName) }}
|
||||
{{ range $address := $all.Cfg.BindAddressIpv4 }}
|
||||
listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol {{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
|
||||
{{ else }}
|
||||
|
|
@ -747,17 +760,17 @@ stream {
|
|||
{{ end }}
|
||||
{{ if $all.IsIPV6Enabled }}
|
||||
{{ range $address := $all.Cfg.BindAddressIpv6 }}
|
||||
{{ if not (empty $server.SSLCertificate) }}listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
|
||||
{{ if not (empty $server.SSLCert.PemFileName) }}listen {{ $address }}:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
|
||||
{{ else }}
|
||||
{{ if not (empty $server.SSLCertificate) }}listen [::]:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
|
||||
{{ if not (empty $server.SSLCert.PemFileName) }}listen [::]:{{ if $all.IsSSLPassthroughEnabled }}{{ $all.ListenPorts.SSLProxy }} proxy_protocol{{ else }}{{ $all.ListenPorts.HTTPS }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ end }}{{ end }} {{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}}
|
||||
# PEM sha: {{ $server.SSLPemChecksum }}
|
||||
ssl_certificate {{ $server.SSLCertificate }};
|
||||
ssl_certificate_key {{ $server.SSLCertificate }};
|
||||
{{ if not (empty $server.SSLFullChainCertificate)}}
|
||||
ssl_trusted_certificate {{ $server.SSLFullChainCertificate }};
|
||||
# PEM sha: {{ $server.SSLCert.PemSHA }}
|
||||
ssl_certificate {{ $server.SSLCert.PemFileName }};
|
||||
ssl_certificate_key {{ $server.SSLCert.PemFileName }};
|
||||
{{ if not (empty $server.SSLCert.FullChainPemFileName)}}
|
||||
ssl_trusted_certificate {{ $server.SSLCert.FullChainPemFileName }};
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
{{ end }}
|
||||
|
|
@ -855,6 +868,17 @@ stream {
|
|||
{{ end }}
|
||||
|
||||
location {{ $path }} {
|
||||
{{ $ing := (getIngressInformation $location.Ingress $location.Path) }}
|
||||
set $namespace "{{ $ing.Namespace }}";
|
||||
set $ingress_name "{{ $ing.Rule }}";
|
||||
set $service_name "{{ $ing.Service }}";
|
||||
set $service_port "{{ $location.Port }}";
|
||||
set $location_path "{{ $location.Path }}";
|
||||
|
||||
{{ if $all.Cfg.EnableOpentracing }}
|
||||
opentracing_propagate_context;
|
||||
{{ end }}
|
||||
|
||||
{{ if not $all.DisableLua }}
|
||||
rewrite_by_lua_block {
|
||||
{{ if $all.DynamicConfigurationEnabled}}
|
||||
|
|
@ -910,10 +934,12 @@ stream {
|
|||
{{ if $all.DynamicConfigurationEnabled}}
|
||||
balancer.log()
|
||||
{{ end }}
|
||||
|
||||
monitor.call()
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ if (and (not (empty $server.SSLCertificate)) $all.Cfg.HSTS) }}
|
||||
{{ if (and (not (empty $server.SSLCert.PemFileName)) $all.Cfg.HSTS) }}
|
||||
if ($scheme = https) {
|
||||
more_set_headers "Strict-Transport-Security: max-age={{ $all.Cfg.HSTSMaxAge }}{{ if $all.Cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }}{{ if $all.Cfg.HSTSPreload }}; preload{{ end }}";
|
||||
}
|
||||
|
|
@ -930,18 +956,10 @@ stream {
|
|||
|
||||
port_in_redirect {{ if $location.UsePortInRedirects }}on{{ else }}off{{ end }};
|
||||
|
||||
{{ if $all.Cfg.EnableVtsStatus }}{{ if $location.VtsFilterKey }} vhost_traffic_status_filter_by_set_key {{ $location.VtsFilterKey }};{{ end }}{{ end }}
|
||||
|
||||
set $proxy_upstream_name "{{ buildUpstreamName $server.Hostname $all.Backends $location $all.DynamicConfigurationEnabled }}";
|
||||
|
||||
{{ $ing := (getIngressInformation $location.Ingress $location.Path) }}
|
||||
{{/* $ing.Metadata contains the Ingress metadata */}}
|
||||
set $namespace "{{ $ing.Namespace }}";
|
||||
set $ingress_name "{{ $ing.Rule }}";
|
||||
set $service_name "{{ $ing.Service }}";
|
||||
|
||||
{{/* redirect to HTTPS can be achieved forcing the redirect or having a SSL Certificate configured for the server */}}
|
||||
{{ if (or $location.Rewrite.ForceSSLRedirect (and (not (empty $server.SSLCertificate)) $location.Rewrite.SSLRedirect)) }}
|
||||
{{ if (or $location.Rewrite.ForceSSLRedirect (and (not (empty $server.SSLCert.PemFileName)) $location.Rewrite.SSLRedirect)) }}
|
||||
{{ if not (isLocationInLocationList $location $all.Cfg.NoTLSRedirectLocations) }}
|
||||
# enforce ssl on server side
|
||||
if ($redirect_to_https) {
|
||||
|
|
@ -980,12 +998,13 @@ stream {
|
|||
auth_request {{ $authPath }};
|
||||
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||
add_header Set-Cookie $auth_cookie;
|
||||
{{- range $idx, $line := buildAuthResponseHeaders $location }}
|
||||
{{- range $line := buildAuthResponseHeaders $location }}
|
||||
{{ $line }}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if $location.ExternalAuth.SigninURL }}
|
||||
set_escape_uri $escaped_request_uri $request_uri;
|
||||
error_page 401 = {{ buildAuthSignURL $location.ExternalAuth.SigninURL }};
|
||||
{{ end }}
|
||||
|
||||
|
|
@ -1113,6 +1132,7 @@ stream {
|
|||
proxy_set_header X-Namespace $namespace;
|
||||
proxy_set_header X-Ingress-Name $ingress_name;
|
||||
proxy_set_header X-Service-Name $service_name;
|
||||
proxy_set_header X-Service-Port $service_port;
|
||||
{{ end }}
|
||||
|
||||
{{ if not (empty $location.Backend) }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue