diff --git a/.requirements b/.requirements index e83a0e24dccd..0acb3686b48f 100644 --- a/.requirements +++ b/.requirements @@ -4,15 +4,15 @@ OPENRESTY=1.25.3.1 LUAROCKS=3.11.0 OPENSSL=3.2.1 PCRE=10.43 -LIBEXPAT=2.5.0 +LIBEXPAT=2.6.2 # Note: git repositories can be loaded from local path if path is set as value -LUA_KONG_NGINX_MODULE=691ba795ced07364d491e8abbdf0c8c8d3778c14 # 0.10.0 +LUA_KONG_NGINX_MODULE=a8411f7cf4289049f0bd3e8e40088e7256389ed3 # 0.11.0 LUA_RESTY_LMDB=7d2581cbe30cde18a8482d820c227ca0845c0ded # 1.4.2 LUA_RESTY_EVENTS=8448a92cec36ac04ea522e78f6496ba03c9b1fd8 # 0.2.0 LUA_RESTY_WEBSOCKET=60eafc3d7153bceb16e6327074e0afc3d94b1316 # 0.4.0 -ATC_ROUTER=1eeb0509a90494dc8618c5cd034ca4be231bb344 # 1.6.1 +ATC_ROUTER=ffd11db657115769bf94f0c4f915f98300bc26b6 # 1.6.2 KONG_MANAGER=nightly NGX_WASM_MODULE=3bd94e61c55415ccfb0f304fa51143a7d630d6ae diff --git a/build/libexpat/repositories.bzl b/build/libexpat/repositories.bzl index 3662761ca78b..d379af1244c0 100644 --- a/build/libexpat/repositories.bzl +++ b/build/libexpat/repositories.bzl @@ -14,7 +14,7 @@ def libexpat_repositories(): http_archive, name = "libexpat", url = "https://github.com/libexpat/libexpat/releases/download/" + tag + "/expat-" + version + ".tar.gz", - sha256 = "6b902ab103843592be5e99504f846ec109c1abb692e85347587f237a4ffa1033", + sha256 = "d4cf38d26e21a56654ffe4acd9cd5481164619626802328506a2869afab29ab3", strip_prefix = "expat-" + version, build_file = "//build/libexpat:BUILD.libexpat.bazel", ) diff --git a/build/openresty/patches/lua-cjson-2.1.0.13_01-error-on-t_end.patch b/build/openresty/patches/lua-cjson-2.1.0.13_01-error-on-t_end.patch new file mode 100644 index 000000000000..3aeaafe5b6f2 --- /dev/null +++ b/build/openresty/patches/lua-cjson-2.1.0.13_01-error-on-t_end.patch @@ -0,0 +1,26 @@ +From e1fca089680e76896744ec2f25219dd705fe21da Mon Sep 17 00:00:00 2001 +From: Wangchong Zhou +Date: Wed, 17 Apr 2024 18:00:10 +0800 +Subject: [PATCH 1/4] bugfix: throw error if T_END found in the middle of input + +--- + lua_cjson.c | 4 ++++ + tests/test.lua | 5 +++++ + 2 files changed, 9 insertions(+) + +diff --git a/bundle/lua-cjson-2.1.0.13/lua_cjson.c b/bundle/lua-cjson-2.1.0.13/lua_cjson.c +index 363466c..7343f32 100644 +--- a/bundle/lua-cjson-2.1.0.13/lua_cjson.c ++++ b/bundle/lua-cjson-2.1.0.13/lua_cjson.c +@@ -1437,6 +1437,10 @@ static int json_decode(lua_State *l) + if (token.type != T_END) + json_throw_parse_error(l, &json, "the end", &token); + ++ /* Make sure T_END (\x00) doesn't occur at middle of input */ ++ if (json.data + json_len > json.ptr) ++ json_throw_parse_error(l, &json, "EOF", &token); ++ + strbuf_free(json.tmp); + + return 1; + diff --git a/changelog/unreleased/kong/bump-atc-router.yml b/changelog/unreleased/kong/bump-atc-router.yml index 9005de32f50e..8206d52592f6 100644 --- a/changelog/unreleased/kong/bump-atc-router.yml +++ b/changelog/unreleased/kong/bump-atc-router.yml @@ -1,3 +1,3 @@ -message: "Bumped atc-router from v1.6.0 to v1.6.1" +message: "Bumped atc-router from v1.6.0 to v1.6.2" type: dependency scope: Core diff --git a/changelog/unreleased/kong/bump-libexpat.yml b/changelog/unreleased/kong/bump-libexpat.yml new file mode 100644 index 000000000000..e83a65ed2994 --- /dev/null +++ b/changelog/unreleased/kong/bump-libexpat.yml @@ -0,0 +1,3 @@ +message: "Bumped libexpat to 2.6.2" +type: dependency +scope: Core diff --git a/changelog/unreleased/kong/bump-lua-kong-nginx-module.yml b/changelog/unreleased/kong/bump-lua-kong-nginx-module.yml index b66d912e4c3d..c4749b3327a8 100644 --- a/changelog/unreleased/kong/bump-lua-kong-nginx-module.yml +++ b/changelog/unreleased/kong/bump-lua-kong-nginx-module.yml @@ -1,3 +1,4 @@ -message: "Bumped lua-kong-nginx-module from 0.8.0 to 0.10.0" +message: | + Bumped lua-kong-nginx-module from 0.8.0 to 0.11.0 type: dependency scope: Core diff --git a/changelog/unreleased/kong/bump-lua-resty-acme.yml b/changelog/unreleased/kong/bump-lua-resty-acme.yml new file mode 100644 index 000000000000..74a484b85b6d --- /dev/null +++ b/changelog/unreleased/kong/bump-lua-resty-acme.yml @@ -0,0 +1,3 @@ +message: "Bumped lua-resty-acme to 0.13.0" +type: dependency +scope: Core diff --git a/changelog/unreleased/kong/bump-lua-resty-http-0.17.2.yml b/changelog/unreleased/kong/bump-lua-resty-http-0.17.2.yml new file mode 100644 index 000000000000..584a721f1789 --- /dev/null +++ b/changelog/unreleased/kong/bump-lua-resty-http-0.17.2.yml @@ -0,0 +1,2 @@ +message: Bump lua-resty-http to 0.17.2. +type: dependency diff --git a/changelog/unreleased/kong/bump-lua-resty-openssl.yml b/changelog/unreleased/kong/bump-lua-resty-openssl.yml index 7e43d0456f79..cac749e60c76 100644 --- a/changelog/unreleased/kong/bump-lua-resty-openssl.yml +++ b/changelog/unreleased/kong/bump-lua-resty-openssl.yml @@ -1,3 +1,3 @@ -message: "Bumped lua-resty-openssl to 1.2.1" +message: Bumped lua-resty-openssl from 1.2.0 to 1.3.1 type: dependency scope: Core diff --git a/changelog/unreleased/kong/fix-cjson-t-end.yml b/changelog/unreleased/kong/fix-cjson-t-end.yml new file mode 100644 index 000000000000..3977de7b12f0 --- /dev/null +++ b/changelog/unreleased/kong/fix-cjson-t-end.yml @@ -0,0 +1,3 @@ +message: | + Improve the robustness of lua-cjson when handling unexpected input. +type: dependency diff --git a/changelog/unreleased/kong/fix-dbless-duplicate-target-error.yml b/changelog/unreleased/kong/fix-dbless-duplicate-target-error.yml new file mode 100644 index 000000000000..082e5dd52180 --- /dev/null +++ b/changelog/unreleased/kong/fix-dbless-duplicate-target-error.yml @@ -0,0 +1,3 @@ +message: "Fixed an issue wherein `POST /config?flatten_errors=1` could not return a proper response if the input included duplicate upstream targets" +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/fix-vault-secret-update-without-ttl.yml b/changelog/unreleased/kong/fix-vault-secret-update-without-ttl.yml new file mode 100644 index 000000000000..fe0622057e8b --- /dev/null +++ b/changelog/unreleased/kong/fix-vault-secret-update-without-ttl.yml @@ -0,0 +1,3 @@ +message: Fixed a bug that allowed vault secrets to refresh even when they had no TTL set. +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/fix-wasm-disable-pwm-lua-resolver.yml b/changelog/unreleased/kong/fix-wasm-disable-pwm-lua-resolver.yml new file mode 100644 index 000000000000..f8c49499ee91 --- /dev/null +++ b/changelog/unreleased/kong/fix-wasm-disable-pwm-lua-resolver.yml @@ -0,0 +1,4 @@ +message: | + Disable usage of the Lua DNS resolver from proxy-wasm by default. +type: bugfix +scope: Configuration diff --git a/changelog/unreleased/kong/update-ai-proxy-telemetry.yml b/changelog/unreleased/kong/update-ai-proxy-telemetry.yml index e4ac98afa760..fcb68b218de6 100644 --- a/changelog/unreleased/kong/update-ai-proxy-telemetry.yml +++ b/changelog/unreleased/kong/update-ai-proxy-telemetry.yml @@ -1,3 +1,3 @@ -message: Update telemetry collection for AI Plugins to allow multiple instances data to be set for the same request. +message: Update telemetry collection for AI Plugins to allow multiple plugins data to be set for the same request. type: bugfix scope: Core diff --git a/kong-3.7.0-0.rockspec b/kong-3.7.0-0.rockspec index 62441d1ae594..8aeb8719bef1 100644 --- a/kong-3.7.0-0.rockspec +++ b/kong-3.7.0-0.rockspec @@ -16,7 +16,7 @@ dependencies = { "luasec == 1.3.2", "luasocket == 3.0-rc1", "penlight == 1.14.0", - "lua-resty-http == 0.17.1", + "lua-resty-http == 0.17.2", "lua-resty-jit-uuid == 0.0.7", "lua-ffi-zlib == 0.6", "multipart == 0.5.9", @@ -34,10 +34,10 @@ dependencies = { "lua-resty-healthcheck == 3.0.1", "lua-messagepack == 0.5.4", "lua-resty-aws == 1.4.1", - "lua-resty-openssl == 1.2.1", + "lua-resty-openssl == 1.3.1", "lua-resty-counter == 0.2.1", "lua-resty-ipmatcher == 0.6.1", - "lua-resty-acme == 0.12.0", + "lua-resty-acme == 0.13.0", "lua-resty-session == 4.0.5", "lua-resty-timer-ng == 0.2.7", "lpeg == 1.1.0", diff --git a/kong/clustering/compat/checkers.lua b/kong/clustering/compat/checkers.lua index 2cc89cba3821..2a900f0728e1 100644 --- a/kong/clustering/compat/checkers.lua +++ b/kong/clustering/compat/checkers.lua @@ -47,6 +47,7 @@ local compatible_checkers = { return has_update end, }, + { 3005000000, --[[ 3.5.0.0 ]] function(config_table, dp_version, log_suffix) local has_update diff --git a/kong/clustering/compat/init.lua b/kong/clustering/compat/init.lua index cb4b4245ebf4..5cafaf67d926 100644 --- a/kong/clustering/compat/init.lua +++ b/kong/clustering/compat/init.lua @@ -402,4 +402,42 @@ function _M.update_compatible_payload(payload, dp_version, log_suffix) end +-- If mixed config is detected and a 3.6 or lower DP is attached to the CP, +-- no config will be sent at all +function _M.check_mixed_route_entities(payload, dp_version, flavor) + if flavor ~= "expressions" then + return true + end + + -- CP runs with 'expressions' flavor + + local dp_version_num = version_num(dp_version) + + if dp_version_num >= 3007000000 then -- [[ 3.7.0.0 ]] + return true + end + + local routes = payload["config_table"].routes or {} + local routes_n = #routes + local count = 0 -- expression route count + + for i = 1, routes_n do + local r = routes[i] + + -- expression should be a string + if r.expression and r.expression ~= ngx.null then + count = count + 1 + end + end + + if count == routes_n or -- all are expression only routes + count == 0 -- all are traditional routes + then + return true + end + + return false, dp_version .. " does not support mixed mode route" +end + + return _M diff --git a/kong/clustering/control_plane.lua b/kong/clustering/control_plane.lua index aec39586c99a..33d427424e7c 100644 --- a/kong/clustering/control_plane.lua +++ b/kong/clustering/control_plane.lua @@ -39,6 +39,7 @@ local sleep = ngx.sleep local plugins_list_to_map = compat.plugins_list_to_map local update_compatible_payload = compat.update_compatible_payload +local check_mixed_route_entities = compat.check_mixed_route_entities local deflate_gzip = require("kong.tools.gzip").deflate_gzip local yield = require("kong.tools.yield").yield local connect_dp = clustering_utils.connect_dp @@ -432,6 +433,15 @@ function _M:handle_cp_websocket(cert) goto continue end + ok, err = check_mixed_route_entities(self.reconfigure_payload, dp_version, + kong and kong.configuration and + kong.configuration.router_flavor) + if not ok then + ngx_log(ngx_WARN, _log_prefix, "unable to send updated configuration to data plane: ", err, log_suffix) + + goto continue + end + local _, deflated_payload, err = update_compatible_payload(self.reconfigure_payload, dp_version, log_suffix) if not deflated_payload then -- no modification or err, use the cached payload diff --git a/kong/conf_loader/init.lua b/kong/conf_loader/init.lua index bb36dde41e9f..9f51ef8c6d12 100644 --- a/kong/conf_loader/init.lua +++ b/kong/conf_loader/init.lua @@ -563,7 +563,8 @@ local function load(path, custom_conf, opts) -- set it as such in kong_defaults, because it can only be used if wasm is -- _also_ enabled. We inject it here if the user has not opted to set it -- themselves. - add_wasm_directive("nginx_http_proxy_wasm_lua_resolver", "on") + -- TODO: as a temporary compatibility fix, we are forcing it to 'off'. + add_wasm_directive("nginx_http_proxy_wasm_lua_resolver", "off") -- wasm vm properties are inherited from previously set directives if conf.lua_ssl_trusted_certificate and #conf.lua_ssl_trusted_certificate >= 1 then diff --git a/kong/db/errors.lua b/kong/db/errors.lua index 7139c636ddb6..67276bf41b2b 100644 --- a/kong/db/errors.lua +++ b/kong/db/errors.lua @@ -795,12 +795,19 @@ do ---@param err_t table ---@param flattened table local function add_entity_errors(entity_type, entity, err_t, flattened) - if type(err_t) ~= "table" or nkeys(err_t) == 0 then + local err_type = type(err_t) + + -- promote error strings to `@entity` type errors + if err_type == "string" then + err_t = { ["@entity"] = err_t } + + elseif err_type ~= "table" or nkeys(err_t) == 0 then return + end -- this *should* be unreachable, but it's relatively cheap to guard against -- compared to everything else we're doing in this code path - elseif type(entity) ~= "table" then + if type(entity) ~= "table" then log(WARN, "could not parse ", entity_type, " errors for non-table ", "input: '", tostring(entity), "'") return @@ -1033,13 +1040,7 @@ do for i, err_t_i in drain(section_errors) do local entity = entities[i] - - -- promote error strings to `@entity` type errors - if type(err_t_i) == "string" then - err_t_i = { ["@entity"] = err_t_i } - end - - if type(entity) == "table" and type(err_t_i) == "table" then + if type(entity) == "table" then add_entity_errors(entity_type, entity, err_t_i, flattened) else diff --git a/kong/db/schema/others/declarative_config.lua b/kong/db/schema/others/declarative_config.lua index 15d291f6c0b3..455c6fa129fc 100644 --- a/kong/db/schema/others/declarative_config.lua +++ b/kong/db/schema/others/declarative_config.lua @@ -335,7 +335,7 @@ local function uniqueness_error_msg(entity, key, value) end -local function populate_references(input, known_entities, by_id, by_key, expected, errs, parent_entity) +local function populate_references(input, known_entities, by_id, by_key, expected, errs, parent_entity, parent_idx) for _, entity in ipairs(known_entities) do yield(true) @@ -363,7 +363,7 @@ local function populate_references(input, known_entities, by_id, by_key, expecte for i, item in ipairs(input[entity]) do yield(true) - populate_references(item, known_entities, by_id, by_key, expected, errs, entity) + populate_references(item, known_entities, by_id, by_key, expected, errs, entity, i) local item_id = DeclarativeConfig.pk_string(entity_schema, item) local key = use_key and item[endpoint_key] @@ -381,8 +381,23 @@ local function populate_references(input, known_entities, by_id, by_key, expecte if item_id then by_id[entity] = by_id[entity] or {} if (not failed) and by_id[entity][item_id] then - errs[entity] = errs[entity] or {} - errs[entity][i] = uniqueness_error_msg(entity, "primary key", item_id) + local err_t + + if parent_entity and parent_idx then + errs[parent_entity] = errs[parent_entity] or {} + errs[parent_entity][parent_idx] = errs[parent_entity][parent_idx] or {} + errs[parent_entity][parent_idx][entity] = errs[parent_entity][parent_idx][entity] or {} + + -- e.g. errs["upstreams"][5]["targets"] + err_t = errs[parent_entity][parent_idx][entity] + + else + errs[entity] = errs[entity] or {} + err_t = errs[entity] + end + + err_t[i] = uniqueness_error_msg(entity, "primary key", item_id) + else by_id[entity][item_id] = item table.insert(by_id[entity], item_id) diff --git a/kong/llm/drivers/shared.lua b/kong/llm/drivers/shared.lua index 69c89d9d5c51..041062a724db 100644 --- a/kong/llm/drivers/shared.lua +++ b/kong/llm/drivers/shared.lua @@ -9,11 +9,13 @@ local parse_url = require("socket.url").parse -- local log_entry_keys = { - REQUEST_BODY = "ai.payload.request", - RESPONSE_BODY = "payload.response", - TOKENS_CONTAINER = "usage", META_CONTAINER = "meta", + PAYLOAD_CONTAINER = "payload", + REQUEST_BODY = "ai.payload.request", + + -- payload keys + RESPONSE_BODY = "response", -- meta keys REQUEST_MODEL = "request_model", @@ -35,33 +37,6 @@ _M.streaming_has_token_counts = { ["llama2"] = true, } ---- Splits a table key into nested tables. --- Each part of the key separated by dots represents a nested table. --- @param obj The table to split keys for. --- @return A nested table structure representing the split keys. -local function split_table_key(obj) - local result = {} - - for key, value in pairs(obj) do - local keys = {} - for k in key:gmatch("[^.]+") do - table.insert(keys, k) - end - - local currentTable = result - for i, k in ipairs(keys) do - if i < #keys then - currentTable[k] = currentTable[k] or {} - currentTable = currentTable[k] - else - currentTable[k] = value - end - end - end - - return result -end - _M.upstream_url_format = { openai = fmt("%s://api.openai.com:%s", (openai_override and "http") or "https", (openai_override) or "443"), anthropic = "https://api.anthropic.com:443", @@ -302,76 +277,65 @@ function _M.post_request(conf, response_object) if conf.logging and conf.logging.log_statistics then local provider_name = conf.model.provider + local plugin_name = conf.__key__:match('plugins:(.-):') + if not plugin_name or plugin_name == "" then + return nil, "no plugin name is being passed by the plugin" + end + -- check if we already have analytics in this context local request_analytics = kong.ctx.shared.analytics - -- create a new try context - local current_try = { - [log_entry_keys.META_CONTAINER] = {}, - [log_entry_keys.TOKENS_CONTAINER] = {}, - } - -- create a new structure if not if not request_analytics then request_analytics = {} end -- check if we already have analytics for this provider - local request_analytics_provider = request_analytics[provider_name] + local request_analytics_plugin = request_analytics[plugin_name] -- create a new structure if not - if not request_analytics_provider then - request_analytics_provider = { - request_prompt_tokens = 0, - request_completion_tokens = 0, - request_total_tokens = 0, - number_of_instances = 0, - instances = {}, + if not request_analytics_plugin then + request_analytics_plugin = { + [log_entry_keys.META_CONTAINER] = {}, + [log_entry_keys.PAYLOAD_CONTAINER] = {}, + [log_entry_keys.TOKENS_CONTAINER] = { + [log_entry_keys.PROMPT_TOKEN] = 0, + [log_entry_keys.COMPLETION_TOKEN] = 0, + [log_entry_keys.TOTAL_TOKENS] = 0, + }, } end -- Set the model, response, and provider names in the current try context - current_try[log_entry_keys.META_CONTAINER][log_entry_keys.REQUEST_MODEL] = conf.model.name - current_try[log_entry_keys.META_CONTAINER][log_entry_keys.RESPONSE_MODEL] = response_object.model or conf.model.name - current_try[log_entry_keys.META_CONTAINER][log_entry_keys.PROVIDER_NAME] = provider_name - current_try[log_entry_keys.META_CONTAINER][log_entry_keys.PLUGIN_ID] = conf.__plugin_id + request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.REQUEST_MODEL] = conf.model.name + request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.RESPONSE_MODEL] = response_object.model or conf.model.name + request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.PROVIDER_NAME] = provider_name + request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.PLUGIN_ID] = conf.__plugin_id -- Capture openai-format usage stats from the transformed response body if response_object.usage then if response_object.usage.prompt_tokens then - request_analytics_provider.request_prompt_tokens = (request_analytics_provider.request_prompt_tokens + response_object.usage.prompt_tokens) - current_try[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.PROMPT_TOKEN] = response_object.usage.prompt_tokens + request_analytics_plugin[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.PROMPT_TOKEN] = request_analytics_plugin[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.PROMPT_TOKEN] + response_object.usage.prompt_tokens end if response_object.usage.completion_tokens then - request_analytics_provider.request_completion_tokens = (request_analytics_provider.request_completion_tokens + response_object.usage.completion_tokens) - current_try[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.COMPLETION_TOKEN] = response_object.usage.completion_tokens + request_analytics_plugin[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.COMPLETION_TOKEN] = request_analytics_plugin[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.COMPLETION_TOKEN] + response_object.usage.completion_tokens end if response_object.usage.total_tokens then - request_analytics_provider.request_total_tokens = (request_analytics_provider.request_total_tokens + response_object.usage.total_tokens) - current_try[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.TOTAL_TOKENS] = response_object.usage.total_tokens + request_analytics_plugin[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.TOTAL_TOKENS] = request_analytics_plugin[log_entry_keys.TOKENS_CONTAINER][log_entry_keys.TOTAL_TOKENS] + response_object.usage.total_tokens end end -- Log response body if logging payloads is enabled if conf.logging and conf.logging.log_payloads then - current_try[log_entry_keys.RESPONSE_BODY] = body_string + request_analytics_plugin[log_entry_keys.PAYLOAD_CONTAINER][log_entry_keys.RESPONSE_BODY] = body_string end - -- Increment the number of instances - request_analytics_provider.number_of_instances = request_analytics_provider.number_of_instances + 1 - - -- Get the current try count - local try_count = request_analytics_provider.number_of_instances - - -- Store the split key data in instances - request_analytics_provider.instances[try_count] = split_table_key(current_try) - -- Update context with changed values - request_analytics[provider_name] = request_analytics_provider + request_analytics[plugin_name] = request_analytics_plugin kong.ctx.shared.analytics = request_analytics -- Log analytics data - kong.log.set_serialize_value(fmt("%s.%s", "ai", provider_name), request_analytics_provider) + kong.log.set_serialize_value(fmt("%s.%s", "ai", plugin_name), request_analytics_plugin) end return nil diff --git a/kong/pdk/vault.lua b/kong/pdk/vault.lua index 347c3d050f83..4a29f405aff8 100644 --- a/kong/pdk/vault.lua +++ b/kong/pdk/vault.lua @@ -769,7 +769,23 @@ local function new(self) else lru_ttl = ttl - shdict_ttl = DAO_MAX_TTL + -- shared dict ttl controls when the secret + -- value will be refreshed by `rotate_secrets` + -- timer. If a secret whose remaining time is less + -- than `config.resurrect_ttl`(or DAO_MAX_TTL + -- if not configured), it could possibly + -- be updated in every cycle of `rotate_secrets`. + -- + -- The shdict_ttl should be + -- `config.ttl` + `config.resurrect_ttl` + -- to make sure the secret value persists for + -- at least `config.ttl` seconds. + -- When `config.resurrect_ttl` is not set and + -- `config.ttl` is not set, shdict_ttl will be + -- DAO_MAX_TTL * 2; when `config.resurrect_ttl` + -- is not set but `config.ttl` is set, shdict_ttl + -- will be ttl + DAO_MAX_TTL + shdict_ttl = ttl + DAO_MAX_TTL end else diff --git a/kong/plugins/ai-request-transformer/handler.lua b/kong/plugins/ai-request-transformer/handler.lua index 7553877660a5..9517be366325 100644 --- a/kong/plugins/ai-request-transformer/handler.lua +++ b/kong/plugins/ai-request-transformer/handler.lua @@ -45,6 +45,7 @@ function _M:access(conf) -- first find the configured LLM interface and driver local http_opts = create_http_opts(conf) conf.llm.__plugin_id = conf.__plugin_id + conf.llm.__key__ = conf.__key__ local ai_driver, err = llm:new(conf.llm, http_opts) if not ai_driver then diff --git a/kong/plugins/ai-response-transformer/handler.lua b/kong/plugins/ai-response-transformer/handler.lua index 7fd4a2900b79..7014d8938526 100644 --- a/kong/plugins/ai-response-transformer/handler.lua +++ b/kong/plugins/ai-response-transformer/handler.lua @@ -98,6 +98,7 @@ function _M:access(conf) -- first find the configured LLM interface and driver local http_opts = create_http_opts(conf) conf.llm.__plugin_id = conf.__plugin_id + conf.llm.__key__ = conf.__key__ local ai_driver, err = llm:new(conf.llm, http_opts) if not ai_driver then diff --git a/kong/resty/dns/client.lua b/kong/resty/dns/client.lua index 2d929fae1e36..f36c492d75c2 100644 --- a/kong/resty/dns/client.lua +++ b/kong/resty/dns/client.lua @@ -1511,7 +1511,7 @@ local function execute_toip(qname, port, dnsCacheOnly, try_list, force_no_sync) -- our SRV entry might still contain a hostname, so recurse, with found port number local srvport = (entry.port ~= 0 and entry.port) or port -- discard port if it is 0 add_status_to_try_list(try_list, "dereferencing SRV") - return execute_toip(entry.target, srvport, dnsCacheOnly, try_list) + return execute_toip(entry.target, srvport, dnsCacheOnly, try_list, force_no_sync) end -- must be A or AAAA diff --git a/kong/router/transform.lua b/kong/router/transform.lua index dbb693f394ee..b3600d45ebe2 100644 --- a/kong/router/transform.lua +++ b/kong/router/transform.lua @@ -1,6 +1,5 @@ local bit = require("bit") local buffer = require("string.buffer") -local tb_new = require("table.new") local tb_nkeys = require("table.nkeys") local uuid = require("resty.jit-uuid") local lrucache = require("resty.lrucache") @@ -717,48 +716,64 @@ local function group_by(t, f) return result end --- split routes into multiple routes, one for each prefix length and one for all --- regular expressions -local function split_route_by_path_info(route_and_service, routes_and_services_split) - local original_route = route_and_service.route +-- split routes into multiple routes, +-- one for each prefix length and one for all regular expressions +local function split_routes_and_services_by_path(routes_and_services) + local count = #routes_and_services + local append_count = 1 - if is_empty_field(original_route.paths) or #original_route.paths == 1 or - not is_null(original_route.expression) -- expression will ignore paths - then - tb_insert(routes_and_services_split, route_and_service) - return - end + for i = 1, count do + local route_and_service = routes_and_services[i] + local original_route = route_and_service.route + local original_paths = original_route.paths - -- make sure that route_and_service contains only the two expected entries, route and service - assert(tb_nkeys(route_and_service) == 1 or tb_nkeys(route_and_service) == 2) + if is_empty_field(original_paths) or #original_paths == 1 or + not is_null(original_route.expression) -- expression will ignore paths + then + goto continue + end - local grouped_paths = group_by( - original_route.paths, sort_by_regex_or_length - ) - for index, paths in pairs(grouped_paths) do - local cloned_route = { - route = shallow_copy(original_route), - service = route_and_service.service, - } + -- make sure that route_and_service contains + -- only the two expected entries, route and service + local nkeys = tb_nkeys(route_and_service) + assert(nkeys == 1 or nkeys == 2) - cloned_route.route.original_route = original_route - cloned_route.route.paths = paths - cloned_route.route.id = uuid_generator(original_route.id .. "#" .. tostring(index)) + local original_route_id = original_route.id + local original_service = route_and_service.service - tb_insert(routes_and_services_split, cloned_route) - end -end + -- `grouped_paths` is a hash table, like {true={'regex'}, 2={'/a'}, 3={'/aa'},} + local grouped_paths = group_by(original_paths, sort_by_regex_or_length) + local is_first = true + for key, paths in pairs(grouped_paths) do + local route = shallow_copy(original_route) -local function split_routes_and_services_by_path(routes_and_services) - local count = #routes_and_services - local routes_and_services_split = tb_new(count, 0) + -- create a new route from the original route + route.original_route = original_route + route.paths = paths + route.id = uuid_generator(original_route_id .. "#" .. tostring(key)) - for i = 1, count do - split_route_by_path_info(routes_and_services[i], routes_and_services_split) - end + local cloned_route_and_service = { + route = route, + service = original_service, + } + + if is_first then + -- the first one will replace the original route + routes_and_services[i] = cloned_route_and_service + is_first = false + + else + -- the others will append to the original routes array + routes_and_services[count + append_count] = cloned_route_and_service + append_count = append_count + 1 + end + end -- for pairs(grouped_paths) + + ::continue:: + end -- for routes_and_services - return routes_and_services_split + return routes_and_services end diff --git a/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt b/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt index 58bce910f670..3f759cfe241a 100644 --- a/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt +++ b/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt @@ -55,7 +55,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libstdc++.so.6 - libm.so.6 @@ -206,4 +206,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt b/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt index 23c8f07c567b..1475038b6e31 100644 --- a/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt +++ b/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt @@ -50,7 +50,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libstdc++.so.6 - libm.so.6 @@ -192,4 +192,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt b/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt index 0c21f6338a99..f2e42a900694 100644 --- a/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt +++ b/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt @@ -40,7 +40,7 @@ - libc.so.6 Rpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libm.so.6 - libc.so.6 @@ -173,4 +173,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/debian-10-amd64.txt b/scripts/explain_manifest/fixtures/debian-10-amd64.txt index 8c717069d615..cfc938ae3499 100644 --- a/scripts/explain_manifest/fixtures/debian-10-amd64.txt +++ b/scripts/explain_manifest/fixtures/debian-10-amd64.txt @@ -55,7 +55,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libstdc++.so.6 - libm.so.6 @@ -206,4 +206,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/debian-11-amd64.txt b/scripts/explain_manifest/fixtures/debian-11-amd64.txt index 63ff912a9513..5eed8dc87a71 100644 --- a/scripts/explain_manifest/fixtures/debian-11-amd64.txt +++ b/scripts/explain_manifest/fixtures/debian-11-amd64.txt @@ -55,7 +55,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libc.so.6 @@ -195,4 +195,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/debian-12-amd64.txt b/scripts/explain_manifest/fixtures/debian-12-amd64.txt index 199749ff81c5..687623bfeb2c 100644 --- a/scripts/explain_manifest/fixtures/debian-12-amd64.txt +++ b/scripts/explain_manifest/fixtures/debian-12-amd64.txt @@ -50,7 +50,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libc.so.6 @@ -182,4 +182,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/el7-amd64.txt b/scripts/explain_manifest/fixtures/el7-amd64.txt index 3724e68614bc..95122d3d6501 100644 --- a/scripts/explain_manifest/fixtures/el7-amd64.txt +++ b/scripts/explain_manifest/fixtures/el7-amd64.txt @@ -55,7 +55,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libstdc++.so.6 - libm.so.6 @@ -205,4 +205,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/el8-amd64.txt b/scripts/explain_manifest/fixtures/el8-amd64.txt index d27b15850ccd..1553481b6141 100644 --- a/scripts/explain_manifest/fixtures/el8-amd64.txt +++ b/scripts/explain_manifest/fixtures/el8-amd64.txt @@ -55,7 +55,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libstdc++.so.6 - libm.so.6 @@ -205,4 +205,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/el9-amd64.txt b/scripts/explain_manifest/fixtures/el9-amd64.txt index 28f3047b1cb7..bfe5a9a06fa3 100644 --- a/scripts/explain_manifest/fixtures/el9-amd64.txt +++ b/scripts/explain_manifest/fixtures/el9-amd64.txt @@ -50,7 +50,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libstdc++.so.6 - libm.so.6 @@ -192,4 +192,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/el9-arm64.txt b/scripts/explain_manifest/fixtures/el9-arm64.txt index 0c21f6338a99..f2e42a900694 100644 --- a/scripts/explain_manifest/fixtures/el9-arm64.txt +++ b/scripts/explain_manifest/fixtures/el9-arm64.txt @@ -40,7 +40,7 @@ - libc.so.6 Rpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libm.so.6 - libc.so.6 @@ -173,4 +173,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt b/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt index 9b346da59c8f..d0103ac00df7 100644 --- a/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt +++ b/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt @@ -55,7 +55,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt b/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt index 77d0ab4be01a..443c3426f7f8 100644 --- a/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt +++ b/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt @@ -50,7 +50,7 @@ - libc.so.6 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libc.so.6 @@ -186,4 +186,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt b/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt index 28a22626734f..12545ec6e5fe 100644 --- a/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt +++ b/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt @@ -37,7 +37,7 @@ - ld-linux-aarch64.so.1 Runpath : /usr/local/kong/lib -- Path : /usr/local/kong/lib/libexpat.so.1.8.10 +- Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - libc.so.6 - ld-linux-aarch64.so.1 @@ -184,4 +184,3 @@ OpenSSL : OpenSSL 3.2.1 30 Jan 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True - diff --git a/spec/01-unit/04-prefix_handler_spec.lua b/spec/01-unit/04-prefix_handler_spec.lua index 601b6eebc423..eb0dfd76c7a9 100644 --- a/spec/01-unit/04-prefix_handler_spec.lua +++ b/spec/01-unit/04-prefix_handler_spec.lua @@ -927,25 +927,32 @@ describe("NGINX conf compiler", function() end) it("injects default configurations if wasm=on", function() assert.matches( - ".+proxy_wasm_lua_resolver on;.+", + ".+proxy_wasm_lua_resolver off;.+", kong_ngx_cfg({ wasm = true, }, debug) ) end) it("does not inject default configurations if wasm=off", function() assert.not_matches( - ".+proxy_wasm_lua_resolver on;.+", + ".+proxy_wasm_lua_resolver.+", kong_ngx_cfg({ wasm = false, }, debug) ) end) - it("permits overriding proxy_wasm_lua_resolver", function() + it("permits overriding proxy_wasm_lua_resolver to off", function() assert.matches( ".+proxy_wasm_lua_resolver off;.+", kong_ngx_cfg({ wasm = true, - -- or should this be `false`? IDK nginx_http_proxy_wasm_lua_resolver = "off", }, debug) ) end) + it("permits overriding proxy_wasm_lua_resolver to on", function() + assert.matches( + ".+proxy_wasm_lua_resolver on;.+", + kong_ngx_cfg({ wasm = true, + nginx_http_proxy_wasm_lua_resolver = "on", + }, debug) + ) + end) it("injects runtime-specific directives (wasmtime)", function() assert.matches( "wasm {.+wasmtime {.+flag flag1 on;.+flag flag2 1m;.+}.+", diff --git a/spec/01-unit/08-router_spec.lua b/spec/01-unit/08-router_spec.lua index 15141c37c0ab..046cb25bd8b0 100644 --- a/spec/01-unit/08-router_spec.lua +++ b/spec/01-unit/08-router_spec.lua @@ -2,6 +2,7 @@ local Router local path_handling_tests = require "spec.fixtures.router_path_handling_tests" local uuid = require("kong.tools.utils").uuid local get_expression = require("kong.router.transform").get_expression +local deep_copy = require("kong.tools.table").deep_copy local function reload_router(flavor, subsystem) _G.kong = { @@ -3726,7 +3727,8 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" } lazy_setup(function() - router = assert(new_router(use_case_routes)) + -- deep copy use_case_routes in case it changes + router = assert(new_router(deep_copy(use_case_routes))) end) it("strips the specified paths from the given uri if matching", function() @@ -3780,7 +3782,8 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }, } - local router = assert(new_router(use_case_routes)) + -- deep copy use_case_routes in case it changes + local router = assert(new_router(deep_copy(use_case_routes))) local _ngx = mock_ngx("POST", "/my-route/hello/world", { host = "domain.org" }) @@ -4963,7 +4966,8 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }, } - router = assert(new_router(use_case)) + -- deep copy use_case in case it changes + router = assert(new_router(deep_copy(use_case))) end) it("[assigns different priorities to regex and non-regex path]", function() @@ -5011,7 +5015,8 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" }, } - router = assert(new_router(use_case)) + -- deep copy use_case in case it changes + router = assert(new_router(deep_copy(use_case))) end) it("[assigns different priorities to each path]", function() diff --git a/spec/01-unit/19-hybrid/03-compat_spec.lua b/spec/01-unit/19-hybrid/03-compat_spec.lua index b2a0030aa0f0..ebec56368c3d 100644 --- a/spec/01-unit/19-hybrid/03-compat_spec.lua +++ b/spec/01-unit/19-hybrid/03-compat_spec.lua @@ -630,5 +630,121 @@ describe("kong.clustering.compat", function() assert.is_nil(assert(services[3]).ca_certificates) end) - end) + end) -- describe + + describe("route entities compatible changes", function() + local function reload_modules(flavor) + _G.kong = { configuration = { router_flavor = flavor } } + _G.kong.db = nil + + package.loaded["kong.db.schema.entities.routes"] = nil + package.loaded["kong.db.schema.entities.routes_subschemas"] = nil + package.loaded["spec.helpers"] = nil + package.loaded["kong.clustering.compat"] = nil + package.loaded["kong.db.declarative"] = nil + + require("kong.db.schema.entities.routes") + require("kong.db.schema.entities.routes_subschemas") + + compat = require("kong.clustering.compat") + helpers = require ("spec.helpers") + declarative = require("kong.db.declarative") + end + + lazy_setup(function() + reload_modules("expressions") + end) + + lazy_teardown(function() + reload_modules() + end) + + it("won't update with mixed mode routes in expressions flavor lower than 3.7", function() + local _, db = helpers.get_db_utils(nil, { + "routes", + }) + _G.kong.db = db + + -- mixed mode routes + assert(declarative.load_into_db({ + routes = { + route1 = { + protocols = { "http" }, + id = "00000000-0000-0000-0000-000000000001", + hosts = { "example.com" }, + expression = ngx.null, + }, + route2 = { + protocols = { "http" }, + id = "00000000-0000-0000-0000-000000000002", + expression = [[http.path == "/foo"]], + }, + }, + }, { _transform = true })) + + local config = { config_table = declarative.export_config() } + + local ok, err = compat.check_mixed_route_entities(config, "3.6.0", "expressions") + assert.is_false(ok) + assert(string.find(err, "does not support mixed mode route")) + + local ok, err = compat.check_mixed_route_entities(config, "3.7.0", "expressions") + assert.is_true(ok) + assert.is_nil(err) + end) + + it("updates with all traditional routes in expressions flavor", function() + local _, db = helpers.get_db_utils(nil, { + "routes", + }) + _G.kong.db = db + + assert(declarative.load_into_db({ + routes = { + route1 = { + protocols = { "http" }, + id = "00000000-0000-0000-0000-000000000001", + hosts = { "example.com" }, + expression = ngx.null, + }, + }, + }, { _transform = true })) + + local config = { config_table = declarative.export_config() } + + local ok, err = compat.check_mixed_route_entities(config, "3.6.0", "expressions") + assert.is_true(ok) + assert.is_nil(err) + end) + + it("updates with all expression routes in expressions flavor", function() + local _, db = helpers.get_db_utils(nil, { + "routes", + }) + _G.kong.db = db + + assert(declarative.load_into_db({ + routes = { + route1 = { + protocols = { "http" }, + id = "00000000-0000-0000-0000-000000000001", + expression = [[http.path == "/foo"]], + }, + route2 = { + protocols = { "http" }, + id = "00000000-0000-0000-0000-000000000002", + expression = [[http.path == "/bar"]], + }, + }, + }, { _transform = true })) + + local config = { config_table = declarative.export_config() } + + local ok, err = compat.check_mixed_route_entities(config, "3.6.0", "expressions") + assert.is_true(ok) + assert.is_nil(err) + end) + + end) -- describe + end) diff --git a/spec/02-integration/04-admin_api/15-off_spec.lua b/spec/02-integration/04-admin_api/15-off_spec.lua index 3ca5d34b80e8..eee02e99e9f6 100644 --- a/spec/02-integration/04-admin_api/15-off_spec.lua +++ b/spec/02-integration/04-admin_api/15-off_spec.lua @@ -2724,6 +2724,115 @@ R6InCcH2Wh8wSeY5AuDXvu2tv9g/PW9wIJmPuKSHMA== }, }, flattened) end) + + it("correctly handles duplicate upstream target errors", function() + local target = { + target = "10.244.0.12:80", + weight = 1, + tags = { "target-1" }, + } + -- this has the same : tuple as the first target, so it will + -- be assigned the same id + local dupe_target = utils.deep_copy(target) + dupe_target.tags = { "target-2" } + + local input = { + _format_version = "3.0", + services = { + { + connect_timeout = 60000, + host = "httproute.default.httproute-testing.0", + id = "4e3cb785-a8d0-5866-aa05-117f7c64f24d", + name = "httproute.default.httproute-testing.0", + port = 8080, + protocol = "http", + read_timeout = 60000, + retries = 5, + routes = { + { + https_redirect_status_code = 426, + id = "073fc413-1c03-50b4-8f44-43367c13daba", + name = "httproute.default.httproute-testing.0.0", + path_handling = "v0", + paths = { + "~/httproute-testing$", + "/httproute-testing/", + }, + preserve_host = true, + protocols = { + "http", + "https", + }, + strip_path = true, + tags = {}, + }, + }, + tags = {}, + write_timeout = 60000, + }, + }, + upstreams = { + { + algorithm = "round-robin", + name = "httproute.default.httproute-testing.0", + id = "e9792964-6797-482c-bfdf-08220a4f6832", + tags = { + "k8s-name:httproute-testing", + "k8s-namespace:default", + "k8s-kind:HTTPRoute", + "k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839", + "k8s-group:gateway.networking.k8s.io", + "k8s-version:v1", + }, + targets = { + { + target = "10.244.0.11:80", + weight = 1, + }, + { + target = "10.244.0.12:80", + weight = 1, + }, + }, + }, + { + algorithm = "round-robin", + name = "httproute.default.httproute-testing.1", + id = "f9792964-6797-482c-bfdf-08220a4f6839", + tags = { + "k8s-name:httproute-testing", + "k8s-namespace:default", + "k8s-kind:HTTPRoute", + "k8s-uid:f9792964-6797-482c-bfdf-08220a4f6839", + "k8s-group:gateway.networking.k8s.io", + "k8s-version:v1", + }, + targets = { + target, + dupe_target, + }, + }, + }, + } + + local flattened = post_config(input) + local entry = get_by_tag(dupe_target.tags[1], flattened) + assert.not_nil(entry, "no error for duplicate target in the response") + + -- sanity + assert.same(dupe_target.tags, entry.entity_tags) + + assert.is_table(entry.errors, "missing entity errors table") + assert.equals(1, #entry.errors, "expected 1 entity error") + assert.is_table(entry.errors[1], "entity error is not a table") + + local e = entry.errors[1] + assert.equals("entity", e.type) + + local exp = string.format("uniqueness violation: 'targets' entity with primary key set to '%s' already declared", entry.entity_id) + + assert.equals(exp, e.message) + end) end) diff --git a/spec/02-integration/13-vaults/05-ttl_spec.lua b/spec/02-integration/13-vaults/05-ttl_spec.lua index 5ad2d7ec0747..8066c942ed01 100644 --- a/spec/02-integration/13-vaults/05-ttl_spec.lua +++ b/spec/02-integration/13-vaults/05-ttl_spec.lua @@ -218,6 +218,123 @@ describe("vault ttl and rotation (#" .. strategy .. ") #" .. vault.name, functio end) end) +describe("vault rotation #without ttl (#" .. strategy .. ") #" .. vault.name, function() + local client + local secret = "my-secret" + + + local function http_get(path) + path = path or "/" + + local res = client:get(path, { + headers = { + host = assert(vault.host), + }, + }) + + assert.response(res).has.status(200) + + return res + end + + + lazy_setup(function() + helpers.setenv("KONG_LUA_PATH_OVERRIDE", LUA_PATH) + helpers.setenv("KONG_VAULT_ROTATION_INTERVAL", "1") + + vault:setup() + + local bp = helpers.get_db_utils(strategy, + { "vaults", "routes", "services", "plugins" }, + { "dummy" }, + { vault.name }) + + + -- override a default config without default ttl + assert(bp.vaults:insert({ + name = vault.name, + prefix = vault.prefix, + config = { + default_value = "init", + }, + })) + + local route = assert(bp.routes:insert({ + name = vault.host, + hosts = { vault.host }, + paths = { "/" }, + service = assert(bp.services:insert()), + })) + + + -- used by the plugin config test case + assert(bp.plugins:insert({ + name = "dummy", + config = { + resp_header_value = fmt("{vault://%s/%s}", + vault.prefix, secret), + }, + route = { id = route.id }, + })) + + assert(helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + vaults = vault.name, + plugins = "dummy", + log_level = "info", + }, nil, nil, vault:fixtures() )) + + client = helpers.proxy_client() + end) + + + lazy_teardown(function() + if client then + client:close() + end + + helpers.stop_kong() + vault:teardown() + + helpers.unsetenv("KONG_LUA_PATH_OVERRIDE") + end) + + + it("update secret value should not refresh cached vault reference(backend: #" .. vault.name .. ")", function() + local function check_plugin_secret(expect, ttl, leeway) + leeway = leeway or 0.25 -- 25% + + local timeout = ttl + (ttl * leeway) + + -- The secret value is supposed to be not refreshed + -- after several rotations + assert.has_error(function() + assert + .with_timeout(timeout) + .with_step(0.5) + .eventually(function() + local res = http_get("/") + local value = assert.response(res).has.header(DUMMY_HEADER) + + if value == expect then + return true + end + + return false + end) + .is_falsy("expected plugin secret not to be updated to '" .. expect .. "' " + .. "' within " .. tostring(timeout) .. "seconds") + end) + end + + vault:update_secret(secret, "old") + check_plugin_secret("init", 5) + + vault:update_secret(secret, "new") + check_plugin_secret("init", 5) + end) +end) describe("#hybrid mode dp vault ttl and rotation (#" .. strategy .. ") #" .. vault.name, function() local client diff --git a/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua b/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua index 96e610f78fe8..d0c264694b64 100644 --- a/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua +++ b/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua @@ -786,7 +786,7 @@ describe("proxy-wasm filters (#wasm) (#" .. strategy .. ")", function() assert.logfile().has.no.line("[crit]", true, 0) end) - it("resolves DNS hostnames to send an http dispatch, return its response body", function() + pending("resolves DNS hostnames to send an http dispatch, return its response body", function() local client = helpers.proxy_client() finally(function() client:close() end) diff --git a/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua b/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua index b9d8c31888d4..c81d8ab1255c 100644 --- a/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua +++ b/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua @@ -35,26 +35,19 @@ local function wait_for_json_log_entry(FILE_LOG_PATH) end local _EXPECTED_CHAT_STATS = { - openai = { - instances = { - { - meta = { - plugin_id = '6e7c40f6-ce96-48e4-a366-d109c169e444', - provider_name = 'openai', - request_model = 'gpt-3.5-turbo', - response_model = 'gpt-3.5-turbo-0613', - }, - usage = { - completion_token = 12, - prompt_token = 25, - total_tokens = 37, - }, - }, + ["ai-proxy"] = { + meta = { + plugin_id = '6e7c40f6-ce96-48e4-a366-d109c169e444', + provider_name = 'openai', + request_model = 'gpt-3.5-turbo', + response_model = 'gpt-3.5-turbo-0613', + }, + payload = {}, + usage = { + completion_token = 12, + prompt_token = 25, + total_tokens = 37, }, - number_of_instances = 1, - request_completion_tokens = 12, - request_prompt_tokens = 25, - request_total_tokens = 37, }, } @@ -691,9 +684,9 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.matches('"role": "user"', log_message.ai.payload.request, nil, true) -- test response bodies - assert.matches('"content": "The sum of 1 + 1 is 2.",', log_message.ai.openai.instances[1].payload.response, nil, true) - assert.matches('"role": "assistant"', log_message.ai.openai.instances[1].payload.response, nil, true) - assert.matches('"id": "chatcmpl-8T6YwgvjQVVnGbJ2w8hpOA17SeNy2"', log_message.ai.openai.instances[1].payload.response, nil, true) + assert.matches('"content": "The sum of 1 + 1 is 2.",', log_message.ai["ai-proxy"].payload.response, nil, true) + assert.matches('"role": "assistant"', log_message.ai["ai-proxy"].payload.response, nil, true) + assert.matches('"id": "chatcmpl-8T6YwgvjQVVnGbJ2w8hpOA17SeNy2"', log_message.ai["ai-proxy"].payload.response, nil, true) end) it("internal_server_error request", function() diff --git a/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua b/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua index 662fb4c9e11a..2711f4aa393f 100644 --- a/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua +++ b/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua @@ -1,11 +1,41 @@ local helpers = require "spec.helpers" local cjson = require "cjson" +local pl_file = require "pl.file" +local pl_stringx = require "pl.stringx" local MOCK_PORT = helpers.get_available_port() local PLUGIN_NAME = "ai-request-transformer" +local FILE_LOG_PATH_STATS_ONLY = os.tmpname() + +local function wait_for_json_log_entry(FILE_LOG_PATH) + local json + + assert + .with_timeout(10) + .ignore_exceptions(true) + .eventually(function() + local data = assert(pl_file.read(FILE_LOG_PATH)) + + data = pl_stringx.strip(data) + assert(#data > 0, "log file is empty") + + data = data:match("%b{}") + assert(data, "log file does not contain JSON") + + json = cjson.decode(data) + end) + .has_no_error("log file contains a valid JSON entry") + + return json +end + local OPENAI_FLAT_RESPONSE = { route_type = "llm/v1/chat", + logging = { + log_payloads = false, + log_statistics = true, + }, model = { name = "gpt-4", provider = "openai", @@ -84,6 +114,23 @@ local EXPECTED_RESULT_FLAT = { } } +local _EXPECTED_CHAT_STATS = { + ["ai-request-transformer"] = { + meta = { + plugin_id = '71083e79-4921-4f9f-97a4-ee7810b6cd8a', + provider_name = 'openai', + request_model = 'gpt-4', + response_model = 'gpt-3.5-turbo-0613', + }, + payload = {}, + usage = { + completion_token = 12, + prompt_token = 25, + total_tokens = 37, + }, + }, +} + local SYSTEM_PROMPT = "You are a mathematician. " .. "Multiply all numbers in my JSON request, by 2." @@ -142,6 +189,7 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then }) bp.plugins:insert { name = PLUGIN_NAME, + id = "71083e79-4921-4f9f-97a4-ee7810b6cd8a", route = { id = without_response_instructions.id }, config = { prompt = SYSTEM_PROMPT, @@ -149,6 +197,14 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then }, } + bp.plugins:insert { + name = "file-log", + route = { id = without_response_instructions.id }, + config = { + path = FILE_LOG_PATH_STATS_ONLY, + }, + } + local bad_request = assert(bp.routes:insert { paths = { "/echo-bad-request" } }) @@ -216,6 +272,29 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.same(EXPECTED_RESULT_FLAT, body_table.post_data.params) end) + it("logs statistics", function() + local r = client:get("/echo-flat", { + headers = { + ["content-type"] = "application/json", + ["accept"] = "application/json", + }, + body = REQUEST_BODY, + }) + + local body = assert.res_status(200 , r) + local _, err = cjson.decode(body) + + assert.is_nil(err) + + local log_message = wait_for_json_log_entry(FILE_LOG_PATH_STATS_ONLY) + assert.same("127.0.0.1", log_message.client_ip) + assert.is_number(log_message.request.size) + assert.is_number(log_message.response.size) + + -- test ai-proxy stats + assert.same(_EXPECTED_CHAT_STATS, log_message.ai) + end) + it("bad request from LLM", function() local r = client:get("/echo-bad-request", { headers = { diff --git a/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua b/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua index 2fdd5b11e71f..13e4b558a3ef 100644 --- a/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua +++ b/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua @@ -1,9 +1,35 @@ local helpers = require "spec.helpers" local cjson = require "cjson" +local pl_file = require "pl.file" +local pl_stringx = require "pl.stringx" local MOCK_PORT = helpers.get_available_port() local PLUGIN_NAME = "ai-response-transformer" +local FILE_LOG_PATH_STATS_ONLY = os.tmpname() + +local function wait_for_json_log_entry(FILE_LOG_PATH) + local json + + assert + .with_timeout(10) + .ignore_exceptions(true) + .eventually(function() + local data = assert(pl_file.read(FILE_LOG_PATH)) + + data = pl_stringx.strip(data) + assert(#data > 0, "log file is empty") + + data = data:match("%b{}") + assert(data, "log file does not contain JSON") + + json = cjson.decode(data) + end) + .has_no_error("log file contains a valid JSON entry") + + return json +end + local OPENAI_INSTRUCTIONAL_RESPONSE = { route_type = "llm/v1/chat", model = { @@ -23,6 +49,10 @@ local OPENAI_INSTRUCTIONAL_RESPONSE = { local OPENAI_FLAT_RESPONSE = { route_type = "llm/v1/chat", + logging = { + log_payloads = false, + log_statistics = true, + }, model = { name = "gpt-4", provider = "openai", @@ -141,6 +171,23 @@ local EXPECTED_RESULT = { }, } +local _EXPECTED_CHAT_STATS = { + ["ai-response-transformer"] = { + meta = { + plugin_id = 'da587462-a802-4c22-931a-e6a92c5866d1', + provider_name = 'openai', + request_model = 'gpt-4', + response_model = 'gpt-3.5-turbo-0613', + }, + payload = {}, + usage = { + completion_token = 12, + prompt_token = 25, + total_tokens = 37, + }, + }, +} + local SYSTEM_PROMPT = "You are a mathematician. " .. "Multiply all numbers in my JSON request, by 2. Return me this message: " .. "{\"status\": 400, \"headers: {\"content-type\": \"application/xml\"}, \"body\": \"OUTPUT\"} " @@ -228,6 +275,7 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then }) bp.plugins:insert { name = PLUGIN_NAME, + id = "da587462-a802-4c22-931a-e6a92c5866d1", route = { id = without_response_instructions.id }, config = { prompt = SYSTEM_PROMPT, @@ -236,6 +284,14 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then }, } + bp.plugins:insert { + name = "file-log", + route = { id = without_response_instructions.id }, + config = { + path = FILE_LOG_PATH_STATS_ONLY, + }, + } + local bad_instructions = assert(bp.routes:insert { paths = { "/echo-bad-instructions" } }) @@ -345,6 +401,29 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.same(EXPECTED_RESULT_FLAT, body_table) end) + it("logs statistics", function() + local r = client:get("/echo-flat", { + headers = { + ["content-type"] = "application/json", + ["accept"] = "application/json", + }, + body = REQUEST_BODY, + }) + + local body = assert.res_status(200 , r) + local _, err = cjson.decode(body) + + assert.is_nil(err) + + local log_message = wait_for_json_log_entry(FILE_LOG_PATH_STATS_ONLY) + assert.same("127.0.0.1", log_message.client_ip) + assert.is_number(log_message.request.size) + assert.is_number(log_message.response.size) + + -- test ai-proxy stats + assert.same(_EXPECTED_CHAT_STATS, log_message.ai) + end) + it("fails properly when json instructions are bad", function() local r = client:get("/echo-bad-instructions", { headers = {