This is an automated email from the ASF dual-hosted git repository. ashishtiwari pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push: new 2b774e5a5 fix(consumer): missed consumer update due to wrong version in cache (#12413) 2b774e5a5 is described below commit 2b774e5a55c0a578c8717d74589b2d9f3429ae76 Author: Ashish Tiwari <ashishjaitiwari15112...@gmail.com> AuthorDate: Fri Jul 11 10:33:04 2025 +0530 fix(consumer): missed consumer update due to wrong version in cache (#12413) --- apisix/consumer.lua | 33 ++++++--- t/admin/consumer-credentials.t | 161 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 12 deletions(-) diff --git a/apisix/consumer.lua b/apisix/consumer.lua index d69226b96..97b4c999f 100644 --- a/apisix/consumer.lua +++ b/apisix/consumer.lua @@ -94,7 +94,7 @@ do count = consumers_count_for_lrucache }) -local function construct_consumer_data(val, plugin_config) +local function construct_consumer_data(val, name, plugin_config) -- if the val is a Consumer, clone it to the local consumer; -- if the val is a Credential, to get the Consumer by consumer_name and then clone -- it to the local consumer. @@ -103,20 +103,28 @@ local function construct_consumer_data(val, plugin_config) local consumer_name = get_consumer_name_from_credential_etcd_key(val.key) local the_consumer = consumers:get(consumer_name) if the_consumer and the_consumer.value then - consumer = core.table.clone(the_consumer.value) - consumer.modifiedIndex = the_consumer.modifiedIndex - consumer.credential_id = get_credential_id_from_etcd_key(val.key) + consumer = consumers_id_lrucache(val.value.id .. name, val.modifiedIndex.. + the_consumer.modifiedIndex, + function (val, the_consumer) + consumer = core.table.clone(the_consumer.value) + consumer.modifiedIndex = the_consumer.modifiedIndex + consumer.credential_id = get_credential_id_from_etcd_key(val.key) + return consumer + end, val, the_consumer) else -- Normally wouldn't get here: -- it should belong to a consumer for any credential. - core.log.error("failed to get the consumer for the credential,", + return nil, "failed to get the consumer for the credential,", " a wild credential has appeared!", - " credential key: ", val.key, ", consumer name: ", consumer_name) - return nil, "failed to get the consumer for the credential" + " credential key: ", val.key, ", consumer name: ", consumer_name end else - consumer = core.table.clone(val.value) - consumer.modifiedIndex = val.modifiedIndex + consumer = consumers_id_lrucache(val.value.id .. name, val.modifiedIndex, + function (val) + consumer = core.table.clone(val.value) + consumer.modifiedIndex = val.modifiedIndex + return consumer + end, val) end -- if the consumer has labels, set the field custom_id to it. @@ -160,9 +168,10 @@ function plugin_consumer() } end - local consumer = consumers_id_lrucache(val.value.id .. name, - val.modifiedIndex, construct_consumer_data, val, config) - if consumer == nil then + local consumer, err = construct_consumer_data(val, name, config) + if not consumer then + core.log.error("failed to construct consumer data for plugin ", + name, ": ", err) goto CONTINUE end diff --git a/t/admin/consumer-credentials.t b/t/admin/consumer-credentials.t new file mode 100644 index 000000000..060e253d7 --- /dev/null +++ b/t/admin/consumer-credentials.t @@ -0,0 +1,161 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); +log_level("info"); + +run_tests; + +__DATA__ + +=== TEST 1: Verify consumer plugin update takes effect immediately +--- extra_yaml_config +nginx_config: + worker_processes: 1 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local json = require("toolkit.json") + local http = require("resty.http") + + -- 1. Create route with key-auth plugin + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/anything", + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } + }, + "plugins": { + "key-auth": { + "query": "apikey" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("Route creation failed: " .. body) + return + end + + -- 2. Create consumer jack + code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("Consumer creation failed: " .. body) + return + end + + -- 3. Create credentials for jack + code, body = t('/apisix/admin/consumers/jack/credentials/auth-one', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": { + "key": "auth-one" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("Credential creation failed: " .. body) + return + end + ngx.sleep(0.5) -- wait for etcd to sync + -- 4. Verify valid request succeeds + local httpc = http.new() + local res, err = httpc:request_uri( + "http://127.0.0.1:"..ngx.var.server_port.."/anything?apikey=auth-one", + { method = "GET" } + ) + if not res then + ngx.say("Request failed: ", err) + return + end + + if res.status ~= 200 then + ngx.say("Unexpected status: ", res.status) + ngx.say(res.body) + return + end + + -- 5. Update consumer with fault-injection plugin + code, body = t('/apisix/admin/consumers/jack', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "fault-injection": { + "abort": { + "http_status": 400, + "body": "abort" + } + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("Consumer update failed: " .. body) + return + end + + -- 6. Verify all requests return 400 + for i = 1, 5 do + local res, err = httpc:request_uri( + "http://127.0.0.1:"..ngx.var.server_port.."/anything?apikey=auth-one", + { method = "GET" } + ) + if not res then + ngx.say(i, ": Request failed: ", err) + return + end + + if res.status ~= 400 then + ngx.say(i, ": Expected 400 but got ", res.status) + return + end + + if res.body ~= "abort" then + ngx.say(i, ": Unexpected response body: ", res.body) + return + end + end + + ngx.say("All requests aborted as expected") + } + } +--- request +GET /t +--- response_body +All requests aborted as expected