Skip to content

Commit

Permalink
feat(cli): add 'unready' CLI command to set /status/ready to 503
Browse files Browse the repository at this point in the history
This command updates the `/status/ready` endpoint to return a `503 Service Unavailable` status code.
This allows external tools, such as Kubernetes, to detect when Kong is not ready to receive traffic.
Based on this response, Kubernetes can gracefully remove Kong from its load balancing pool according
to its configured policies, facilitating a smooth shutdown process.

Fix: [FTI-6276](https://konghq.atlassian.net/browse/FTI-6276)
Signed-off-by: tzssangglass <[email protected]>
  • Loading branch information
tzssangglass committed Nov 6, 2024
1 parent 5cab556 commit 5242b56
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 3 deletions.
1 change: 1 addition & 0 deletions bin/kong
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ local cmds = {
roar = true,
hybrid = true,
vault = true,
unready = true,
}

-- unnecessary to inject nginx directives for these simple cmds
Expand Down
3 changes: 3 additions & 0 deletions changelog/unreleased/kong/feat-kong-unready-cmd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
message: Add the `kong unready` CLI command to make the `/status/ready` endpoint return `503 Service Unavailable` response.
type: feature
scope: CLI Command
66 changes: 66 additions & 0 deletions kong/cmd/unready.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
local http = require "resty.luasocket.http"
local print = print
local fmt = string.format

local conf_loader = require "kong.conf_loader"
local pl_path = require "pl.path"
local log = require "kong.cmd.utils.log"

return {
execute = function(args)
log.disable()

-- only to retrieve the default prefix or use given one
local conf = assert(conf_loader(args.conf, {
prefix = args.prefix
}))

if pl_path.exists(conf.kong_env) then
-- load <PREFIX>/kong.conf containing running node's config
conf = assert(conf_loader(conf.kong_env))
end

log.enable()

if #conf.status_listeners == 0 then
print("No status listeners found in configuration.")
return
end

local status_listener = conf.status_listeners[1]

local scheme = "http"
if status_listener.ssl then
scheme = "https"
end

local url = scheme .. "://" .. status_listener.ip .. ":" .. status_listener.port .. "/status/unready"

local httpc = http.new()
httpc:set_timeout(1000)

local res, err = httpc:request_uri(url, {
method = "POST",
headers = {
["Content-Type"] = "application/json"
},
body = "{}",
-- we don't need to verify the SSL certificate for this request
ssl_verify = false,
})

httpc:close()

if not res then
print(fmt("Failed to send request to %s: %s", url, err))
return
end

if res.status ~= 204 then
print(fmt("Unexpected response status from %s: %d", url, res.status))
return
end

print("Kong's status successfully changed to 'unready'")
end
}
24 changes: 21 additions & 3 deletions kong/status/ready.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local PLUGINS_REBUILD_COUNTER_KEY = constants.PLUGINS_REBUILD_COUNTER_KEY
local ROUTERS_REBUILD_COUNTER_KEY = constants.ROUTERS_REBUILD_COUNTER_KEY
local DECLARATIVE_EMPTY_CONFIG_HASH = constants.DECLARATIVE_EMPTY_CONFIG_HASH

local KONG_STATUS_UNREADY = "kong:status:unready"

local function is_dbless_ready(router_rebuilds, plugins_iterator_rebuilds)
if router_rebuilds < worker_count then
Expand Down Expand Up @@ -75,16 +76,16 @@ local function is_ready()
if not ok then
return false, "failed to connect to database"
end

kong.db:close()

if is_control_plane then
return true
end

local router_rebuilds =
local router_rebuilds =
tonumber(kong_shm:get(ROUTERS_REBUILD_COUNTER_KEY)) or 0
local plugins_iterator_rebuilds =
local plugins_iterator_rebuilds =
tonumber(kong_shm:get(PLUGINS_REBUILD_COUNTER_KEY)) or 0

local err
Expand All @@ -102,6 +103,16 @@ end
return {
["/status/ready"] = {
GET = function(self, dao, helpers)
local unready, err = kong_shm:get(KONG_STATUS_UNREADY)
if unready == nil then
kong_shm:set(KONG_STATUS_UNREADY, false)
end

if unready == true then
ngx_log(ngx_DEBUG, "not ready for proxying: unready")
return kong.response.exit(503, { message = "unready" })
end

local ok, err = is_ready()
if ok then
ngx_log(ngx_DEBUG, "ready for proxying")
Expand All @@ -112,5 +123,12 @@ return {
return kong.response.exit(503, { message = err })
end
end
},

["/status/unready"] = {
POST = function(self, dao, helpers)
kong_shm:set(KONG_STATUS_UNREADY, true)
return kong.response.exit(204)
end
}
}
157 changes: 157 additions & 0 deletions spec/02-integration/02-cmd/17-unready_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
local helpers = require "spec.helpers"
local http = require "resty.http"

local dp_status_port = 8100

local function get_status_no_ssl_verify()
local httpc = http.new()

local ok, err = httpc:connect({
scheme = "https",
host = "127.0.0.1",
port = dp_status_port,
ssl_verify = false,
})
if not ok then
return nil, nil, err
end

local res, err = httpc:request({
path = "/status/ready",
headers = {
["Content-Type"] = "application/json",
}
})

if not res then
return nil, nil, err
end

local status = res.status

local body, err = res:read_body()
if not body then
return nil, nil, err
end

httpc:set_keepalive()

return body, status
end

for _, strategy in helpers.each_strategy() do
describe("kong unready with #" .. strategy .. " backend", function()
lazy_setup(function()
helpers.get_db_utils(strategy, {
}) -- runs migrations

assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
status_listen = "127.0.0.1:8100",
nginx_main_worker_processes = 8,
}))
end)

lazy_teardown(function()
helpers.stop_kong()
end)

it("should set Kong to 'unready'", function()
local client = helpers.http_client("127.0.0.1", dp_status_port, 20000)

local res = assert(client:send {
method = "GET",
path = "/status/ready",
})

local status = res and res.status
client:close()

assert.equal(200, status)

local ok, err, msg = helpers.kong_exec("unready", {
prefix = helpers.test_conf.prefix,
})
assert.equal("", err)
assert.equal("Kong's status successfully changed to 'unready'\n", msg)
assert.equal(true, ok)

local client = helpers.http_client("127.0.0.1", dp_status_port, 20000)

local res = assert(client:send {
method = "GET",
path = "/status/ready",
})

local status = res and res.status
client:close()

assert.equal(503, status)
end)

end)

describe("Kong without a status listener", function()
lazy_setup(function()
helpers.get_db_utils(strategy, {
}) -- runs migrations

assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
}))
end)

lazy_teardown(function()
helpers.stop_kong()
end)

it("should return an error when trying to set 'unready' without a status listener", function()
local ok, err, msg = helpers.kong_exec("unready", {
prefix = helpers.test_conf.prefix,
})
assert.equal("", err)
assert.equal("No status listeners found in configuration.\n", msg)
assert.equal(true, ok)
end)

end)

describe("Kong with SSL-enabled status listener", function()
lazy_setup(function()
helpers.get_db_utils(strategy, {
}) -- runs migrations

assert(helpers.start_kong({
database = strategy,
nginx_conf = "spec/fixtures/custom_nginx.template",
status_listen = "127.0.0.1:8100 ssl",
nginx_main_worker_processes = 8,
}))
end)

lazy_teardown(function()
helpers.stop_kong()
end)

it("should set Kong to 'unready' with SSL-enabled status listener", function()
local body, status, err = get_status_no_ssl_verify()
assert.equal(200, status)
assert.equal('{"message":"ready"}', body)
assert.is_nil(err)

local ok, err, msg = helpers.kong_exec("unready", {
prefix = helpers.test_conf.prefix,
})
assert.equal("", err)
assert.equal("Kong's status successfully changed to 'unready'\n", msg)
assert.equal(true, ok)

local body, status, err = get_status_no_ssl_verify()
assert.equal(503, status)
assert.equal('{"message":"unready"}', body)
assert.is_nil(err)
end)
end)
end

0 comments on commit 5242b56

Please sign in to comment.