diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index 4709eb7a5..d7671a79e 100755 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -213,7 +213,7 @@ There is a special mode of upstream hashing called subset. In this mode, upstrea To enable consistent hashing for a backend: -`nginx.ingress.kubernetes.io/upstream-hash-by`: the nginx variable, text value or any combination thereof to use for consistent hashing. For example `nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"` to consistently hash upstream requests by the current request URI. +`nginx.ingress.kubernetes.io/upstream-hash-by`: the nginx variable, text value or any combination thereof to use for consistent hashing. For example: `nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"` or `nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri$host"` or `nginx.ingress.kubernetes.io/upstream-hash-by: "${request_uri}-text-value"` to consistently hash upstream requests by the current request URI. "subset" hashing can be enabled setting `nginx.ingress.kubernetes.io/upstream-hash-by-subset`: "true". This maps requests to subset of nodes instead of a single one. `upstream-hash-by-subset-size` determines the size of each subset (default 3). diff --git a/internal/ingress/annotations/upstreamhashby/main_test.go b/internal/ingress/annotations/upstreamhashby/main_test.go index e67803e51..5a71be56f 100644 --- a/internal/ingress/annotations/upstreamhashby/main_test.go +++ b/internal/ingress/annotations/upstreamhashby/main_test.go @@ -39,6 +39,7 @@ func TestParse(t *testing.T) { expected string }{ {map[string]string{annotation: "$request_uri"}, "$request_uri"}, + {map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme"}, {map[string]string{annotation: "false"}, "false"}, {map[string]string{}, ""}, {nil, ""}, diff --git a/rootfs/etc/nginx/lua/balancer/chash.lua b/rootfs/etc/nginx/lua/balancer/chash.lua index 3eddab9e5..c845e6715 100644 --- a/rootfs/etc/nginx/lua/balancer/chash.lua +++ b/rootfs/etc/nginx/lua/balancer/chash.lua @@ -1,14 +1,21 @@ local balancer_resty = require("balancer.resty") local resty_chash = require("resty.chash") local util = require("util") +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR local _M = balancer_resty:new({ factory = resty_chash, name = "chash" }) function _M.new(self, backend) local nodes = util.get_nodes(backend.endpoints) + local complex_val, err = util.parse_complex_value(backend["upstreamHashByConfig"]["upstream-hash-by"]) + if err ~= nil then + ngx_log(ngx_ERR, "could not parse the value of the upstream-hash-by: ", err) + end + local o = { instance = self.factory:new(nodes), - hash_by = backend["upstreamHashByConfig"]["upstream-hash-by"], + hash_by = complex_val, traffic_shaping_policy = backend.trafficShapingPolicy, alternative_backends = backend.alternativeBackends, } @@ -18,7 +25,7 @@ function _M.new(self, backend) end function _M.balance(self) - local key = util.lua_ngx_var(self.hash_by) + local key = util.generate_var_value(self.hash_by) return self.instance:find(key) end diff --git a/rootfs/etc/nginx/lua/balancer/chashsubset.lua b/rootfs/etc/nginx/lua/balancer/chashsubset.lua index 9599378d6..c098f5b4d 100644 --- a/rootfs/etc/nginx/lua/balancer/chashsubset.lua +++ b/rootfs/etc/nginx/lua/balancer/chashsubset.lua @@ -3,6 +3,8 @@ local resty_chash = require("resty.chash") local util = require("util") +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR local _M = { name = "chashsubset" } @@ -44,10 +46,14 @@ end function _M.new(self, backend) local subset_map, subsets = build_subset_map(backend) + local complex_val, err = util.parse_complex_value(backend["upstreamHashByConfig"]["upstream-hash-by"]) + if err ~= nil then + ngx_log(ngx_ERR, "could not parse the value of the upstream-hash-by: ", err) + end local o = { instance = resty_chash:new(subset_map), - hash_by = backend["upstreamHashByConfig"]["upstream-hash-by"], + hash_by = complex_val, subsets = subsets, current_endpoints = backend.endpoints } @@ -57,7 +63,7 @@ function _M.new(self, backend) end function _M.balance(self) - local key = util.lua_ngx_var(self.hash_by) + local key = util.generate_var_value(self.hash_by) local subset_id = self.instance:find(key) local endpoints = self.subsets[subset_id] local endpoint = endpoints[math.random(#endpoints)] diff --git a/rootfs/etc/nginx/lua/test/util_test.lua b/rootfs/etc/nginx/lua/test/util_test.lua index 6da681662..1b7266953 100644 --- a/rootfs/etc/nginx/lua/test/util_test.lua +++ b/rootfs/etc/nginx/lua/test/util_test.lua @@ -1,4 +1,6 @@ local original_ngx = ngx +local util = require("util") + local function reset_ngx() _G.ngx = original_ngx end @@ -9,28 +11,43 @@ local function mock_ngx(mock) _G.ngx = _ngx end -describe("lua_ngx_var", function() - local util = require("util") +describe("utility", function() after_each(function() reset_ngx() end) - describe("lua_ngx_var", function() + describe("ngx_complex_value", function() before_each(function() mock_ngx({ var = { remote_addr = "192.168.1.1", [1] = "nginx/regexp/1/group/capturing" } }) end) + local ngx_complex_value = function(data) + local ret, err = util.parse_complex_value(data) + if err ~= nil then + return "" + end + return util.generate_var_value(ret) + end + it("returns value of nginx var by key", function() - assert.equal("192.168.1.1", util.lua_ngx_var("$remote_addr")) + assert.equal("192.168.1.1", ngx_complex_value("$remote_addr")) end) - + it("returns value of nginx var when key is number", function() - assert.equal("nginx/regexp/1/group/capturing", util.lua_ngx_var("$1")) + assert.equal("nginx/regexp/1/group/capturing", ngx_complex_value("$1")) end) - it("returns nil when variable is not defined", function() - assert.equal(nil, util.lua_ngx_var("$foo_bar")) + it("returns value of nginx var by multiple variables", function() + assert.equal("192.168.1.1nginx/regexp/1/group/capturing", ngx_complex_value("$remote_addr$1")) + end) + + it("returns value by the combination of variable and text value", function() + assert.equal("192.168.1.1-text-value", ngx_complex_value("${remote_addr}-text-value")) + end) + + it("returns empty when variable is not defined", function() + assert.equal("", ngx_complex_value("$foo_bar")) end) end) diff --git a/rootfs/etc/nginx/lua/util.lua b/rootfs/etc/nginx/lua/util.lua index 79e88bd2f..c8020dbd3 100644 --- a/rootfs/etc/nginx/lua/util.lua +++ b/rootfs/etc/nginx/lua/util.lua @@ -1,14 +1,14 @@ local string = string local string_len = string.len -local string_sub = string.sub local string_format = string.format local pairs = pairs +local ipairs = ipairs local tonumber = tonumber local getmetatable = getmetatable local type = type local next = next local table = table - +local re_gmatch = ngx.re.gmatch local _M = {} @@ -24,15 +24,57 @@ function _M.get_nodes(endpoints) return nodes end --- given an Nginx variable i.e $request_uri --- it returns value of ngx.var[request_uri] -function _M.lua_ngx_var(ngx_var) - local var_name = string_sub(ngx_var, 2) - if var_name:match("^%d+$") then - var_name = tonumber(var_name) +-- parse the compound variables, then call generate_var_value function +-- to parse into a string value. +function _M.parse_complex_value(complex_value) + local reg = [[ (\\\$[0-9a-zA-Z_]+) | ]] -- \$var + .. [[ \$\{([0-9a-zA-Z_]+)\} | ]] -- ${var} + .. [[ \$([0-9a-zA-Z_]+) | ]] -- $var + .. [[ (\$|[^$\\]+) ]] -- $ or text value + local iterator, err = re_gmatch(complex_value, reg, "jiox") + if not iterator then + return nil, err + end + + local v + local t = {} + while true do + v, err = iterator() + if err then + return nil, err + end + + if not v then + break + end + + table.insert(t, v) + end + + return t +end + +-- Parse the return value of function parse_complex_value +-- into a string value +function _M.generate_var_value(data) + if data == nil then + return "" end - return ngx.var[var_name] + local t = {} + for _, value in ipairs(data) do + local var_name = value[2] or value[3] + if var_name then + if var_name:match("^%d+$") then + var_name = tonumber(var_name) + end + table.insert(t, ngx.var[var_name]) + else + table.insert(t, value[1] or value[4]) + end + end + + return table.concat(t, "") end -- normalize_endpoints takes endpoints as an array of endpoint objects