-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add hash on cookie #3472
Add hash on cookie #3472
Conversation
@@ -748,5 +748,16 @@ return { | |||
CREATE INDEX IF NOT EXISTS ON consumers(custom_id); | |||
CREATE INDEX IF NOT EXISTS ON consumers(username); | |||
]] | |||
}, | |||
{ | |||
name = "2018-05-17-173100_hash_on_cookie", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some automated way contributors are generating these migrations? I just manually created the timestamp/ name and it seemed to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no migrations have to be created manual (for now). Also datastore specific. And indeed the name is date+timestamp, just to make it unique (within a migrations file).
kong/runloop/balancer.lua
Outdated
@@ -656,6 +657,9 @@ local create_hash = function(upstream) | |||
if type(identifier) == "table" then | |||
identifier = table_concat(identifier) | |||
end | |||
|
|||
elseif hash_on == "cookie" then | |||
identifier = ngx.var["cookie_" .. upstream[cookie_field_name]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here you need to store the cookie in the ngx.ctx
table. And if there is no cookie, you need to create it.
additionally, in the header-filter
phase you need to add the cookie to the response
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh great, I misunderstood and thought that a plugin would manage the generation or the cookie, but I think this is a lot better. Thanks for the quick feedback!
@Tieske I think this handles what you were asking for, please take a look. I will add tests of course, but I want to make sure this is on the right path before I go further. |
kong/runloop/handler.lua
Outdated
@@ -707,6 +708,17 @@ return { | |||
header[upstream_status_header] = matches[0] | |||
end | |||
end | |||
|
|||
if ctx.SET_HASH_ON_COOKIE then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put this in the before
hook, presumably this will give plugins the ability to access/ modify the cookie value if there is some use case for it.
kong/runloop/balancer.lua
Outdated
local cookies = get_request_cookies() | ||
table.insert(cookies, cookie_field_name .. "=" .. identifier) | ||
ngx.header["Cookie"] = table_concat(cookies, "; ") | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is all we need here:
elseif hash_on == "cookie" then
identifier = ngx.var["cookie_" .. upstream[cookie_field_name]] or utils.uuid()
ctx.SET_HASH_ON_COOKIE = {
key = upstream[cookie_field_name],
value = identifier,
path = upstream[cookie_path_field_name],
}
We only need to store the retrieved values to insert the cookie on the way out (in the response), no need to add it to the request here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that makes sense. Thanks! Although, I think we don't want to set the cookie again if it already exists, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, you're right! good catch
kong/runloop/handler.lua
Outdated
|
||
insert(cookies, ctx.SET_HASH_ON_COOKIE) | ||
header['Set-Cookie'] = cookies | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the header:
local ck = require "resty.cookie"
and here we do something like:
local cookie_hash_data = ctx.SET_HASH_ON_COOKIE
if cookie_hash_data then
local cookie, ok, err
cookie, err = ck:new()
if cookie then
ok, err = cookie:set(cookie_hash_data)
end
if not ok then
log(ngx.WARN, "failed to set hashing cookie, key=`", cookie_hash_data.key,
"`, value=`", cookie_hash_data.value,
"`, path=`", cookie_hash_data.path,
"` : ", err)
end
end
kong/dao/schemas/upstreams.lua
Outdated
hash_fallback_cookie = { | ||
-- cookie name, if `hash_fallback == "cookie"` | ||
type = "string", | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some validation on the contents of the cookie name and path are needed here imo
kong/dao/schemas/upstreams.lua
Outdated
return false, Errors.schema("Hashing on 'cookie', " .. | ||
"but no cookie name provided") | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also relevant here: if the primary is set to "cookie" then the fallback must be "none". Because if the cookie isn't set, it will be created, hence the fallback will never be invoked in that case.
kong/tools/utils.lua
Outdated
@@ -797,16 +797,16 @@ end | |||
-- a-z, A-Z, 0-9 and '-' are allowed. | |||
-- @param name (string) the header name to verify | |||
-- @return the valid header name, or `nil+error` | |||
_M.validate_header_name = function(name) | |||
_M.validate_http_token = function(name, token_type) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cookie name spec technically allows a handful of other characters, but the same is true of headers. Seems fair to generalize these rules for both. Unless there are any objections?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the purpose it serves, I don't see any issues with that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry forget about that, I think we shouldn't change this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood, and I was on the fence about this, too. Do you prefer me to just duplicate the logic for the cookie name validation? The only difference would be the error message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, let's do that for now. Let's get it compete functionality/test wise first and then look at some final things like style and stuff like this.
kong/plugins/key-auth/schema.lua
Outdated
@@ -12,7 +12,7 @@ end | |||
|
|||
local function check_keys(keys) | |||
for _, key in ipairs(keys) do | |||
local res, err = utils.validate_header_name(key, false) | |||
local res, err = utils.validate_http_token(key, "header") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where did this come from?
kong/dao/schemas/upstreams.lua
Outdated
type = "string", | ||
default = "/", | ||
}, | ||
hash_fallback_cookie_path = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this fallback setting can go. Since there can only be one cookie setting be it primary or fallback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this not support falling back to a cookie value if the primary hash_on
is header
or ip
? I can't think of any use cases for it, but currently header
falling back on ip
and vice-versa are supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes it should. But with headers we can have 2 different headers, one for primary, one for fallback. With cookies, there is never more than one since the cookie is created when absent.
Hence having hash_on_cookie
and hash_on_cookie_path
is enough. We do not need hash_fallback_cookie_path
(actually in the current state, hash_fallback_cookie
is missing).
kong/dao/schemas/upstreams.lua
Outdated
type = "string", | ||
default = "/", | ||
}, | ||
hash_fallback_cookie_path = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes it should. But with headers we can have 2 different headers, one for primary, one for fallback. With cookies, there is never more than one since the cookie is created when absent.
Hence having hash_on_cookie
and hash_on_cookie_path
is enough. We do not need hash_fallback_cookie_path
(actually in the current state, hash_fallback_cookie
is missing).
kong/dao/schemas/upstreams.lua
Outdated
@@ -189,6 +191,20 @@ return { | |||
-- header name, if `hash_fallback == "header"` | |||
type = "string", | |||
}, | |||
hash_on_cookie = { | |||
-- cookie name, if `hash_on == "cookie"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be
-- cookie name, if `hash_on` or `hash_fallback` == `"cookie"`
kong/dao/schemas/upstreams.lua
Outdated
type = "string", | ||
}, | ||
hash_on_cookie_path = { | ||
-- cookie name, if `hash_on == "cookie"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be
-- cookie path, if `hash_on` or `hash_fallback` == `"cookie"`
ebd2571
to
f4899c1
Compare
@Tieske Thanks for that excellent feedback! I think the latest changes address your requests, as well as add some more test coverage. PTAL |
kong/tools/utils.lua
Outdated
-- a-z, A-Z, 0-9 and '-' are allowed. | ||
-- @param name (string) the header name to verify | ||
-- @return the valid cookie name, or `nil+error` | ||
_M.validate_cookie_name = function(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've considered other possible ways to DRY this up, but I think they would mostly hurt the readability of the validation. It could be generalized to not refer to "cookie" or "header" at all in the validation message, and give the responsibility of the schema that is applying this validation to a field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the doc comments above still refer to 'header' so need to be updated
end) | ||
|
||
describe("hashing on cookie", function() | ||
it("when the cookie is set", function() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of any edge cases that testing when a cookie is set catch that are not caught in the next example (where the cookie is not set). I would favor removing this test as redundant, but defer to your judgement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed. But there is no test that validates that no cookie will be set if it was already provided, so maybe update this test to do that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: it is good practice to write the intended result of a test in its description, rather than relying on the reader to infer the proper test results by reading the test case... In this case, a name such as does not reply with Set-Cookie if cookie is already set
or something.
kong/tools/utils.lua
Outdated
-- a-z, A-Z, 0-9 and '-' are allowed. | ||
-- @param name (string) the header name to verify | ||
-- @return the valid cookie name, or `nil+error` | ||
_M.validate_cookie_name = function(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the doc comments above still refer to 'header' so need to be updated
kong/runloop/balancer.lua
Outdated
-- If the cookie doesn't exist, create it | ||
if not identifier then | ||
identifier = utils.uuid() | ||
ngx.ctx.SET_HASH_ON_COOKIE = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked the codebase for naming, and I think this should be lower case (only latency tracking is uppercase). Also, "hash_on" might not be most descriptive, so maybe just "balancer_cookie" or "balancer_hash_cookie", to make it clear it belong to the balancer logic.
spec/01-unit/011-balancer_spec.lua
Outdated
hash_on = "cookie", | ||
hash_on_cookie = "Foo", | ||
}) | ||
assert.are.same(crc32(value), hash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we also add validation of the ctx
variables?, so in this case that ngx.ctx.SET_HASH_ON_COOKIE
is NOT created.
And add another test without a cookie set, that shows that ngx.ctx.SET_HASH_ON_COOKIE
is properly created?
@@ -246,7 +269,7 @@ describe("Admin API: #" .. kong_config.database, function() | |||
local json = cjson.decode(body) | |||
assert.same({ message = "Header: bad header name 'not a <> valid <> header name', allowed characters are A-Z, a-z, 0-9, '_', and '-'" }, json) | |||
|
|||
-- Invalid header | |||
-- Invalid fallback header |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
local json = cjson.decode(body) | ||
assert.same({ message = "Cookie name: bad cookie name 'not a <> valid <> cookie name', allowed characters are A-Z, a-z, 0-9, '_', and '-'" }, json) | ||
|
||
-- Invalid cookie |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duplicate of the one above? maybe this is supposed to be the fallback?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, copy/ paste error. I meant to check validating the cookie path on hash fallback.
end) | ||
|
||
describe("hashing on cookie", function() | ||
it("when the cookie is set", function() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed. But there is no test that validates that no cookie will be set if it was already provided, so maybe update this test to do that?
@Tieske just addressed the last round of feedback, PTAL :) |
@PepperTeasdale looks good to me now. Since I already did a lot of reviews on this, I requested @hishamhm for a final review. Final thing to do would be to update the docs at https://github.com/Kong/getkong.org |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good! I only left notes on very minor things like comments to correct and suggested one test entry.
(I noticed that the things that don't match the current style guide, e.g. uppercase in error messages, are there to match the legacy code around it — and the Upstream entity is due to be converted to the new DAO anyway — so I think they are fine as is.)
kong/dao/schemas/upstreams.lua
Outdated
type = "string", | ||
}, | ||
hash_on_cookie_path = { | ||
-- cookie name, if `hash_on` or `hash_fallback` == `"cookie"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: cookie name
→ cookie path
assert.are.same(ngx.ctx.balancer_hash_cookie.key, "Foo") | ||
assert.are.same(ngx.ctx.balancer_hash_cookie.path, "/") | ||
end) | ||
end) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps also add a "cookie" entry to the "fallback" section below at https://github.com/Kong/kong/pull/3472/files#diff-354d3b9f422325fb4a8342988b3a500cR539 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Didn't see that :)
hash_on = "cookie", | ||
hash_on_cookie = "cookiename", | ||
hash_fallback = "header", | ||
hash_fallback_header = "Cool-Header", --> validate case insensitivity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stale comment from copy/paste, please remove
kong/tools/utils.lua
Outdated
@@ -810,4 +810,22 @@ _M.validate_header_name = function(name) | |||
"', allowed characters are A-Z, a-z, 0-9, '_', and '-'" | |||
end | |||
|
|||
--- Validates a cookie name. | |||
-- Checks characters used in a cookie name to be valid | |||
-- a-z, A-Z, 0-9 and '-' are allowed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment mismatch: in the implementation below, _
is allowed as well.
The range of valid characters for cookie names seems to be a weird mess, so sticking to a small and safe subset like the one below seems like a good idea.
ba083fe
to
847d316
Compare
OK @hishamhm thank you for that feedback! This ought to be good now, and I've cleaned up the git history. My only question pertains to updating the docs. I don't see a |
@@ -562,6 +583,27 @@ describe("Balancer", function() | |||
}) | |||
assert.are.same(crc32(table.concat(value)), hash) | |||
end) | |||
describe("cookie", function() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this block is the code same as the block above, but it should exercise "cookie"
as the hash_fallback
entry instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Woops! Fixed
Yes, that would work! The branch has a |
847d316
to
614aadf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kong/runloop/handler.lua
Outdated
@@ -707,6 +709,20 @@ return { | |||
header[upstream_status_header] = matches[0] | |||
end | |||
end | |||
|
|||
if cookie_hash_data then | |||
local cookie, ok, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no benefits here in separating the declaration and initialization of those variables. Let's keep things more concise and declare/initialize them on the same line (by simply moving the local
keyword to the below lines).
kong/runloop/handler.lua
Outdated
ok, err = cookie:set(cookie_hash_data) | ||
|
||
if not ok then | ||
log(ngx.WARN, "failed to set hashing cookie, key=`", cookie_hash_data.key, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the logs produced by Kong use or should use backticks, instead, they use single quotes '
. Let's update this to keep it consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, I am not sure logging the value (just a UUID) is very insightful here. It mostly feels like noise.
A more concise error message which could be more readable and clearer from a user's POV would also mention the failure is related to "load balancing". Something similar to:
log(ngx.WARN, "failed to set the cookie for hash-based load balancing: ",
err, " (key=", key, ", path=", path, ")")
Not the format and conciseness allows us to drop quotes altogether and still remain redable imho.
kong/runloop/handler.lua
Outdated
@@ -690,6 +691,7 @@ return { | |||
before = function(ctx) | |||
local var = ngx.var | |||
local header = ngx.header | |||
local cookie_hash_data = ctx.balancer_hash_cookie |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like there is no point in retrieving this value outside of the ctx.KONG_PROXIED
branch. It would also be more readable if we retrieved it just above the if cookie_hash_data
branch created below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, this gives the impression that it belongs to the ngx.ctx.balancer_address
structure, in which we already store all balancer-related values for decision making and introspection purposes (its name should be revisited already). Doing so also allows us to reduce usage of the top-level ctx
table (which is sensitive to naming conflicts), and self-documentation by adding this field to the ones documented with its initialization code. cc @Tieske @hishamhm
kong/runloop/balancer.lua
Outdated
-- If the cookie doesn't exist, create it | ||
if not identifier then | ||
identifier = utils.uuid() | ||
ngx.ctx.balancer_hash_cookie = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ctx
is already locally cached above in this function, so better use it instead of invoking ngx.ctx
again here.
end) | ||
|
||
describe("hashing on cookie", function() | ||
it("when the cookie is set", function() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note: it is good practice to write the intended result of a test in its description, rather than relying on the reader to infer the proper test results by reading the test case... In this case, a name such as does not reply with Set-Cookie if cookie is already set
or something.
@thibaultcha Thanks for the good CR. I'll await your discussion on storing balancer cookie data the balancer structure. What would be a more descriptive name than |
I'd just go with |
@PepperTeasdale Choosing the proper name, if we were to rename this structure, would belong to another discussion, and should not be included in the scope of this PR. We'll take care of it later :) |
Either way is fine. That structure needs to be renamed (and maybe torn apart?), so imo that is part of/follow up of the SDK. Until that refactor, in which we can take this one along, it doesn't really matter whether it lives inside a badly named structure, or lives outside with a proper name. |
FWIW, from my perspective as someone new to this repo, I would find it a little confusing to find this data in the |
This allows better distribution for sticky sessions when many consumers may be behind the same NAT gateway (e.g. a call center). When the request comes in, we check for the configured cookie name. If not present in the request, a random guid is created and used for balancing and stored in `ngx.ctx` so the cookie can be added in the response header. Because the cookie is created if it doesn't already exist, the `hash_fallback` option is not available when cookies are the primary identifier.
b21bc78
to
4e9581c
Compare
|
||
-- TODO: This should be added the `ngx.ctx.balancer_address` | ||
-- structure, where other balancer-related values are stored, | ||
-- when that is renamed/ refactored. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you ok with this comment to address the need to refactor the balancer structure? cc: @thibaultcha @Tieske
@PepperTeasdale Merged, thank you! I'll open a different PR addressing the naming concern. |
@PepperTeasdale Thanks for the patch. By the way, you are now eligible to receive your very own Contributor T-shirt (details in the link). Enjoy! :) |
Thank you! A+ contributor experience. I look forward contributing more to this and other kong-related projects :) |
Summary
Add ability to hash on cookie. Per a discussion on the feature suggestion board with @Tieske , this adds a
cookie
option for thehash_on
feature, which works very similarly to the `header option. The use case for this is to allow load balancing with cookie injection, creating a sticky session for API consumers who all behind the same NAT gateway. Specifically this is useful for websockets.Full changelog
cookie
forhash_on
andhash_fallback
valuehash_on_cookie
orhash_fallback_cookie
for the name of the cookie to usehash_on_cookie_path
orhash_fallback_cookie_path
to add the path to theSet-Cookie
headerNotes
I'm new to Lua (and, hence, this project), so feel encouraged to tell me everything that's wrong with my code ;) I'm not sure if there's a place for me to add documentation.
I will clean up git history after making any requested changes/ getting final approval.