From 2de0abb1c713105b0ac945c57be84832e46f0b46 Mon Sep 17 00:00:00 2001 From: qianz Date: Sat, 19 Jul 2025 13:06:47 +0800 Subject: [PATCH 01/13] feat: add metadata filtering support to nacos discovery - Add metadata validation schema in schema_def.lua - Implement metadata filtering logic in nacos discovery - Add comprehensive test cases for metadata validation - Update documentation for metadata filtering feature - Fix lint issues and code formatting --- apisix/discovery/nacos/init.lua | 32 ++++++++ apisix/schema_def.lua | 7 ++ ci/pod/docker-compose.first.yml | 7 ++ ci/pod/nacos/service/Dockerfile | 8 +- ci/pod/nacos/service/entrypoint.sh | 43 ++++++++++ docs/en/latest/discovery/nacos.md | 50 ++++++++++++ docs/zh/latest/discovery/nacos.md | 50 ++++++++++++ t/core/schema_def.t | 99 +++++++++++++++++++++++ t/discovery/nacos.t | 121 +++++++++++++++++++++++++++++ 9 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 ci/pod/nacos/service/entrypoint.sh diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua index d4fec7977018..6d2aa02242b0 100644 --- a/apisix/discovery/nacos/init.lua +++ b/apisix/discovery/nacos/init.lua @@ -21,6 +21,7 @@ local http = require('resty.http') local core = require('apisix.core') local ipairs = ipairs local pairs = pairs +local next = next local type = type local math = math local math_random = math.random @@ -54,6 +55,23 @@ local function get_key(namespace_id, group_name, service_name) return namespace_id .. '.' .. group_name .. '.' .. service_name end + +local function metadata_contains(host_metadata, route_metadata) + if not route_metadata or not next(route_metadata) then + return true + end + if not host_metadata or not next(host_metadata) then + return false + end + + for k, v in pairs(route_metadata) do + if host_metadata[k] ~= v then + return false + end + end + return true +end + local function request(request_uri, path, body, method, basic_auth) local url = request_uri .. path log.info('request url:', url) @@ -319,6 +337,7 @@ local function fetch_full_registry(premature) host = host.ip, port = host.port, weight = host.weight or default_weight, + metadata = host.metadata, } -- docs: https://github.com/yidongnan/grpc-spring-boot-starter/pull/496 if is_grpc(scheme) and host.metadata and host.metadata.gRPC_port then @@ -355,6 +374,19 @@ function _M.nodes(service_name, discovery_args) return nil end local nodes = core.json.decode(value) + + -- Apply metadata filtering if specified + local route_metadata = discovery_args and discovery_args.metadata + if route_metadata and next(route_metadata) then + local filtered_nodes = {} + for _, node in ipairs(nodes) do + if metadata_contains(node.metadata, route_metadata) then + core.table.insert(filtered_nodes, node) + end + end + return filtered_nodes + end + return nodes end diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index d8b62088476c..1fb43da34709 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -488,6 +488,13 @@ local upstream_schema = { description = "group name", type = "string", }, + metadata = { + description = "metadata for filtering service instances", + type = "object", + additionalProperties = { + type = "string" + } + }, } }, pass_host = { diff --git a/ci/pod/docker-compose.first.yml b/ci/pod/docker-compose.first.yml index d203a967ddfc..514b49326b34 100644 --- a/ci/pod/docker-compose.first.yml +++ b/ci/pod/docker-compose.first.yml @@ -178,6 +178,9 @@ services: - ci/pod/nacos/env/service.env environment: SUFFIX_NUM: 1 + METADATA_LANE: "a" + METADATA_ENV: "prod" + METADATA_VERSION: "1.0" restart: unless-stopped ports: - "18001:18001" @@ -195,6 +198,8 @@ services: - ci/pod/nacos/env/service.env environment: SUFFIX_NUM: 2 + METADATA_LANE: "b" + METADATA_ENV: "test" restart: unless-stopped ports: - "18002:18001" @@ -213,6 +218,8 @@ services: environment: SUFFIX_NUM: 1 NAMESPACE: test_ns + METADATA_LANE: "b" + METADATA_ENV: "test" restart: unless-stopped ports: - "18003:18001" diff --git a/ci/pod/nacos/service/Dockerfile b/ci/pod/nacos/service/Dockerfile index d279c74972cc..c9e2fa3f848f 100644 --- a/ci/pod/nacos/service/Dockerfile +++ b/ci/pod/nacos/service/Dockerfile @@ -25,8 +25,8 @@ ENV GROUP=${GROUP:-DEFAULT_GROUP} ADD https://raw.githubusercontent.com/api7/nacos-test-service/main/spring-nacos-1.0-SNAPSHOT.jar /app.jar -ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar",\ - "--suffix.num=${SUFFIX_NUM}","--spring.cloud.nacos.discovery.server-addr=${NACOS_ADDR}",\ - "--spring.application.name=${SERVICE_NAME}","--spring.cloud.nacos.discovery.group=${GROUP}",\ - "--spring.cloud.nacos.discovery.namespace=${NAMESPACE}"] +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] EXPOSE 18001 diff --git a/ci/pod/nacos/service/entrypoint.sh b/ci/pod/nacos/service/entrypoint.sh new file mode 100644 index 000000000000..d916d7506713 --- /dev/null +++ b/ci/pod/nacos/service/entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# 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. +# + +# Build Java command with proper environment variable expansion +JAVA_ARGS=( + "-Djava.security.egd=file:/dev/./urandom" + "-jar" + "/app.jar" + "--suffix.num=${SUFFIX_NUM}" + "--spring.cloud.nacos.discovery.server-addr=${NACOS_ADDR}" + "--spring.application.name=${SERVICE_NAME}" + "--spring.cloud.nacos.discovery.group=${GROUP}" + "--spring.cloud.nacos.discovery.namespace=${NAMESPACE}" +) + +# Add metadata dynamically for all METADATA_* environment variables +for var in $(env | grep '^METADATA_' | cut -d= -f1); do + # Convert METADATA_LANE to lane, METADATA_ENV to env, etc. + metadata_key=$(echo "${var#METADATA_}" | tr '[:upper:]' '[:lower:]') + metadata_value=$(eval echo \$${var}) + + if [ -n "${metadata_value}" ]; then + JAVA_ARGS+=("--spring.cloud.nacos.discovery.metadata.${metadata_key}=${metadata_value}") + fi +done + +# Execute Java with expanded arguments +exec java "${JAVA_ARGS[@]}" diff --git a/docs/en/latest/discovery/nacos.md b/docs/en/latest/discovery/nacos.md index 5ebbcee46b49..689ac4edbf6e 100644 --- a/docs/en/latest/discovery/nacos.md +++ b/docs/en/latest/discovery/nacos.md @@ -132,6 +132,7 @@ $ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY: $admin_ | ------------ | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | | namespace_id | string | optional | public | | This parameter is used to specify the namespace of the corresponding service | | group_name | string | optional | DEFAULT_GROUP | | This parameter is used to specify the group of the corresponding service | +| metadata | object | optional | {} | | Filter service instances by metadata using containment matching | #### Specify the namespace @@ -278,3 +279,52 @@ The formatted response as below: } } ``` + +#### Metadata filtering + +APISIX supports filtering service instances based on metadata. When a route is configured with metadata conditions, only service instances whose metadata contains all the key-value pairs specified in the route's `metadata` configuration will be selected. + +Example: If a service instance has metadata `{lane: "a", env: "prod", version: "1.0"}`, it will match routes configured with metadata `{lane: "a"}` or `{lane: "a", env: "prod"}`, but not routes configured with `{lane: "b"}` or `{lane: "a", region: "us"}`. + +Example of routing a request with metadata filtering: + +```shell +$ curl http://127.0.0.1:9180/apisix/admin/routes/5 -H "X-API-KEY: $admin_key" -X PUT -i -d ' +{ + "uri": "/nacosWithMetadata/*", + "upstream": { + "service_name": "APISIX-NACOS", + "type": "roundrobin", + "discovery_type": "nacos", + "discovery_args": { + "metadata": { + "version": "v1" + } + } + } +}' +``` + +This route will only route traffic to service instances that have the metadata field `version` set to `v1`. + +For multiple metadata criteria: + +```shell +$ curl http://127.0.0.1:9180/apisix/admin/routes/6 -H "X-API-KEY: $admin_key" -X PUT -i -d ' +{ + "uri": "/nacosWithMultipleMetadata/*", + "upstream": { + "service_name": "APISIX-NACOS", + "type": "roundrobin", + "discovery_type": "nacos", + "discovery_args": { + "metadata": { + "lane": "a", + "env": "prod" + } + } + } +}' +``` + +This route will only route traffic to service instances that have both `lane: "a"` and `env: "prod"` in their metadata. diff --git a/docs/zh/latest/discovery/nacos.md b/docs/zh/latest/discovery/nacos.md index 370ef17670b1..b0fc35988c96 100644 --- a/docs/zh/latest/discovery/nacos.md +++ b/docs/zh/latest/discovery/nacos.md @@ -132,6 +132,7 @@ $ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY: $admin_ | ------------ | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | | namespace_id | string | 可选 | public | | 服务所在的命名空间 | | group_name | string | 可选 | DEFAULT_GROUP | | 服务所在的组 | +| metadata | object | 可选 | {} | | 使用包含匹配方式根据元数据过滤服务实例 | #### 指定命名空间 @@ -281,3 +282,52 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/4 -H "X-API-KEY: $admin_key" -X } } ``` + +#### 使用元数据过滤服务实例 + +APISIX 支持根据元数据过滤服务实例。当路由配置了元数据条件时,只有服务实例的元数据包含路由配置中指定的所有键值对,该服务实例才会被选中。 + +举例:如果服务实例的元数据是 `{lane: "a", env: "prod", version: "1.0"}`,那么它能匹配配置了元数据 `{lane: "a"}` 或 `{lane: "a", env: "prod"}` 的路由,但不能匹配配置了 `{lane: "b"}` 或 `{lane: "a", region: "us"}` 的路由。 + +使用元数据过滤的路由配置示例: + +```shell +$ curl http://127.0.0.1:9180/apisix/admin/routes/5 -H "X-API-KEY: $admin_key" -X PUT -i -d ' +{ + "uri": "/nacosWithMetadata/*", + "upstream": { + "service_name": "APISIX-NACOS", + "type": "roundrobin", + "discovery_type": "nacos", + "discovery_args": { + "metadata": { + "version": "v1" + } + } + } +}' +``` + +此路由只会将流量转发到元数据字段 `version` 为 `v1` 的服务实例。 + +使用多个元数据条件的示例: + +```shell +$ curl http://127.0.0.1:9180/apisix/admin/routes/6 -H "X-API-KEY: $admin_key" -X PUT -i -d ' +{ + "uri": "/nacosWithMultipleMetadata/*", + "upstream": { + "service_name": "APISIX-NACOS", + "type": "roundrobin", + "discovery_type": "nacos", + "discovery_args": { + "metadata": { + "lane": "a", + "env": "prod" + } + } + } +}' +``` + +此路由只会将流量转发到元数据中同时包含 `lane: "a"` 和 `env: "prod"` 的服务实例。 diff --git a/t/core/schema_def.t b/t/core/schema_def.t index da3bb51f8b26..e026eb8b6f19 100644 --- a/t/core/schema_def.t +++ b/t/core/schema_def.t @@ -237,3 +237,102 @@ passed } --- response_body passed + + + +=== TEST 5: discovery_args metadata validation +--- config + location /t { + content_by_lua_block { + local schema_def = require("apisix.schema_def") + local core = require("apisix.core") + + -- Create a schema that includes discovery_args + local upstream_schema = schema_def.upstream + + -- Test cases using table-driven approach + local test_cases = { + -- Valid cases + { + name = "valid metadata with multiple string values", + should_pass = true, + upstream = { + service_name = "test-service", + discovery_type = "nacos", + type = "roundrobin", + discovery_args = { + namespace_id = "test-ns", + group_name = "test-group", + metadata = { + version = "v1", + env = "prod", + lane = "a" + } + } + } + }, + { + name = "valid metadata with empty object", + should_pass = true, + upstream = { + service_name = "test-service", + discovery_type = "nacos", + type = "roundrobin", + discovery_args = { + metadata = {} + } + } + }, + + -- Invalid cases + { + name = "invalid metadata with non-string values", + should_pass = false, + upstream = { + service_name = "test-service", + discovery_type = "nacos", + type = "roundrobin", + discovery_args = { + metadata = { + version = 123, -- should be string + env = true, -- should be string + count = 456, -- should be string + config = { -- should be string + port = 8080 + } + } + } + }, + expected_error_pattern = "discovery_args.*validation failed.*metadata.*validation failed.*wrong type.*expected string" + }, + } + + -- Execute all test cases + for i, test_case in ipairs(test_cases) do + local ok, err = core.schema.check(upstream_schema, test_case.upstream) + + if test_case.should_pass then + assert(ok, string.format("Test case %d (%s) should pass validation: %s", + i, test_case.name, err or "")) + else + assert(not ok, string.format("Test case %d (%s) should fail validation", + i, test_case.name)) + assert(err ~= nil, string.format("Test case %d (%s) should have error message", + i, test_case.name)) + + -- Execute test case specific error assertions + if test_case.expected_error_pattern then + assert(string.find(err, test_case.expected_error_pattern), + string.format("Test case %d (%s) error should match pattern '%s', but got: %s", + i, test_case.name, test_case.expected_error_pattern, err)) + -- Log the actual error for debugging + ngx.log(ngx.INFO, string.format("Test case %d (%s) actual error: %s", i, test_case.name, err)) + end + end + end + + ngx.say("passed") + } + } +--- response_body +passed diff --git a/t/discovery/nacos.t b/t/discovery/nacos.t index f2ebee57ea7b..9e46ddf58880 100644 --- a/t/discovery/nacos.t +++ b/t/discovery/nacos.t @@ -1066,3 +1066,124 @@ GET /t --- response_body server 1 server 4 + + + +=== TEST 27: get APISIX-NACOS info from NACOS - metadata filtering lane=a (only server1) +--- yaml_config eval: $::yaml_config +--- apisix_yaml +routes: + - + uri: /hello + upstream: + service_name: APISIX-NACOS + discovery_type: nacos + type: roundrobin + discovery_args: + metadata: + lane: "a" +#END +--- pipelined_requests eval +[ + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello", +] +--- response_body_like eval +[ + qr/server 1/, + qr/server 1/, + qr/server 1/, + qr/server 1/, + qr/server 1/, +] + + + +=== TEST 28: get APISIX-NACOS info from NACOS - metadata filtering lane=b (only server2) +--- yaml_config eval: $::yaml_config +--- apisix_yaml +routes: + - + uri: /hello + upstream: + service_name: APISIX-NACOS + discovery_type: nacos + type: roundrobin + discovery_args: + metadata: + lane: "b" +#END +--- pipelined_requests eval +[ + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello", +] +--- response_body_like eval +[ + qr/server 2/, + qr/server 2/, + qr/server 2/, + qr/server 2/, + qr/server 2/, +] + + + +=== TEST 29: get APISIX-NACOS info from NACOS - metadata filtering no match (lane=c) +--- yaml_config eval: $::yaml_config +--- apisix_yaml +routes: + - + uri: /hello + upstream: + service_name: APISIX-NACOS + discovery_type: nacos + type: roundrobin + discovery_args: + metadata: + lane: "c" +#END +--- request +GET /hello +--- error_code: 503 +--- error_log +no valid upstream node + + + +=== TEST 30: get APISIX-NACOS info from NACOS - metadata filtering version=1.0 (only server1) +--- yaml_config eval: $::yaml_config +--- apisix_yaml +routes: + - + uri: /hello + upstream: + service_name: APISIX-NACOS + discovery_type: nacos + type: roundrobin + discovery_args: + metadata: + version: "1.0" +#END +--- pipelined_requests eval +[ + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello", +] +--- response_body_like eval +[ + qr/server 1/, + qr/server 1/, + qr/server 1/, + qr/server 1/, + qr/server 1/, +] From d8cf96df164117627daf3b9cb063375efb63a2f9 Mon Sep 17 00:00:00 2001 From: qianz Date: Sat, 26 Jul 2025 12:01:49 +0800 Subject: [PATCH 02/13] test: test empty metadata --- t/discovery/nacos.t | 61 ++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/t/discovery/nacos.t b/t/discovery/nacos.t index 9e46ddf58880..fd98960574eb 100644 --- a/t/discovery/nacos.t +++ b/t/discovery/nacos.t @@ -1102,7 +1102,7 @@ routes: -=== TEST 28: get APISIX-NACOS info from NACOS - metadata filtering lane=b (only server2) +=== TEST 28: get APISIX-NACOS info from NACOS - metadata filtering empty (load balance between server1 and server2) --- yaml_config eval: $::yaml_config --- apisix_yaml routes: @@ -1114,24 +1114,49 @@ routes: type: roundrobin discovery_args: metadata: - lane: "b" #END ---- pipelined_requests eval -[ - "GET /hello", - "GET /hello", - "GET /hello", - "GET /hello", - "GET /hello", -] ---- response_body_like eval -[ - qr/server 2/, - qr/server 2/, - qr/server 2/, - qr/server 2/, - qr/server 2/, -] +--- config + location /t { + content_by_lua_block { + local http = require("resty.http") + local uri = "http://127.0.0.1:" .. ngx.var.server_port + + -- Wait for 2 seconds for APISIX initialization + ngx.sleep(2) + local httpc = http.new() + local server1_count = 0 + local server2_count = 0 + + -- Send multiple requests to test load balancing + for i = 1, 10 do + local res, err = httpc:request_uri(uri .. "/hello") + if not res then + ngx.log(ngx.ERR, "Request failed: ", err) + else + -- Clean and validate response + local clean_body = res.body:gsub("%s+$", "") + if clean_body == "server 1" then + server1_count = server1_count + 1 + elseif clean_body == "server 2" then + server2_count = server2_count + 1 + else + ngx.log(ngx.ERR, "Invalid response: ", clean_body) + end + end + end + + -- Verify that both servers were used + if server1_count > 0 and server2_count > 0 then + ngx.say("PASS") + else + ngx.say("FAIL") + end + } + } +--- request +GET /t +--- response_body +PASS From a583743ed98bbf5b096a375f1884bf95c006cfd6 Mon Sep 17 00:00:00 2001 From: qianz Date: Sun, 10 Aug 2025 12:46:44 +0800 Subject: [PATCH 03/13] feat: support multi-value matching --- apisix/discovery/nacos/init.lua | 15 ++++++++++++++- apisix/schema_def.lua | 7 +++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua index 6d2aa02242b0..8a20f5f10fcd 100644 --- a/apisix/discovery/nacos/init.lua +++ b/apisix/discovery/nacos/init.lua @@ -65,7 +65,20 @@ local function metadata_contains(host_metadata, route_metadata) end for k, v in pairs(route_metadata) do - if host_metadata[k] ~= v then + local host_value = host_metadata[k] + if not host_value then + return false + end + + -- Multi-value matching: check if host_value matches any value in the array + local found = false + for _, expected_value in ipairs(v) do + if host_value == expected_value then + found = true + break + end + end + if not found then return false end end diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 1fb43da34709..6cb0944d96cf 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -492,10 +492,13 @@ local upstream_schema = { description = "metadata for filtering service instances", type = "object", additionalProperties = { - type = "string" + type = "array", + items = { + type = "string" + } } }, - } + }, }, pass_host = { description = "mod of host passing", From 132c8b27a3a288842f5908ea22e433680e020911 Mon Sep 17 00:00:00 2001 From: qianz Date: Sun, 10 Aug 2025 13:04:10 +0800 Subject: [PATCH 04/13] test(schema): table-driven nacos discovery_args.metadata cases to support multi-value arrays while preserving prior structure This updates the previous metadata validation tests to assert array-of-string values (multi-value matching) and keeps the original table-driven layout for easier review. --- t/core/schema_def.t | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/t/core/schema_def.t b/t/core/schema_def.t index e026eb8b6f19..77698556c547 100644 --- a/t/core/schema_def.t +++ b/t/core/schema_def.t @@ -240,7 +240,7 @@ passed -=== TEST 5: discovery_args metadata validation +=== TEST 5: discovery_args metadata validation (table-driven) --- config location /t { content_by_lua_block { @@ -250,7 +250,7 @@ passed -- Create a schema that includes discovery_args local upstream_schema = schema_def.upstream - -- Test cases using table-driven approach + -- Test cases using table-driven approach (preserve original structure) local test_cases = { -- Valid cases { @@ -264,9 +264,9 @@ passed namespace_id = "test-ns", group_name = "test-group", metadata = { - version = "v1", - env = "prod", - lane = "a" + version = {"v1"}, + env = {"prod"}, + lane = {"a"}, } } } @@ -286,7 +286,26 @@ passed -- Invalid cases { - name = "invalid metadata with non-string values", + name = "invalid metadata with non-array or non-string values", + should_pass = false, + upstream = { + service_name = "test-service", + discovery_type = "nacos", + type = "roundrobin", + discovery_args = { + metadata = { + version = 123, -- should be array + env = true, -- should be array + count = 456, -- should be array + config = { port = 8080 }, -- should be array of strings + lane = {"a", 1} -- mixed types not allowed + } + } + }, + expected_error_pattern = "discovery_args.*metadata" + }, + { + name = "invalid metadata with string instead of array", should_pass = false, upstream = { service_name = "test-service", @@ -294,16 +313,11 @@ passed type = "roundrobin", discovery_args = { metadata = { - version = 123, -- should be string - env = true, -- should be string - count = 456, -- should be string - config = { -- should be string - port = 8080 - } + lane = "a" } } }, - expected_error_pattern = "discovery_args.*validation failed.*metadata.*validation failed.*wrong type.*expected string" + expected_error_pattern = "metadata.*wrong type" }, } @@ -320,12 +334,10 @@ passed assert(err ~= nil, string.format("Test case %d (%s) should have error message", i, test_case.name)) - -- Execute test case specific error assertions if test_case.expected_error_pattern then assert(string.find(err, test_case.expected_error_pattern), string.format("Test case %d (%s) error should match pattern '%s', but got: %s", i, test_case.name, test_case.expected_error_pattern, err)) - -- Log the actual error for debugging ngx.log(ngx.INFO, string.format("Test case %d (%s) actual error: %s", i, test_case.name, err)) end end @@ -336,3 +348,5 @@ passed } --- response_body passed +--- no_error_log +[alert] From c1c2e34d18e6c37331ea373fecbe3bb81ea541c8 Mon Sep 17 00:00:00 2001 From: qianz Date: Sun, 10 Aug 2025 13:27:39 +0800 Subject: [PATCH 05/13] feat: delete useless comment --- t/core/schema_def.t | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/t/core/schema_def.t b/t/core/schema_def.t index 77698556c547..53ee3fb8a789 100644 --- a/t/core/schema_def.t +++ b/t/core/schema_def.t @@ -240,17 +240,15 @@ passed -=== TEST 5: discovery_args metadata validation (table-driven) +=== TEST 5: discovery_args metadata validation --- config location /t { content_by_lua_block { local schema_def = require("apisix.schema_def") local core = require("apisix.core") - -- Create a schema that includes discovery_args local upstream_schema = schema_def.upstream - -- Test cases using table-driven approach (preserve original structure) local test_cases = { -- Valid cases { From 53e340031c3c0efa9047198cbe317f3b64a03351 Mon Sep 17 00:00:00 2001 From: qianz Date: Sun, 10 Aug 2025 13:51:36 +0800 Subject: [PATCH 06/13] test: update tests --- t/discovery/nacos.t | 60 ++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/t/discovery/nacos.t b/t/discovery/nacos.t index fd98960574eb..7f5b1e0e9504 100644 --- a/t/discovery/nacos.t +++ b/t/discovery/nacos.t @@ -1081,7 +1081,7 @@ routes: type: roundrobin discovery_args: metadata: - lane: "a" + lane: ["a"] #END --- pipelined_requests eval [ @@ -1172,7 +1172,7 @@ routes: type: roundrobin discovery_args: metadata: - lane: "c" + lane: ["c"] #END --- request GET /hello @@ -1182,7 +1182,7 @@ no valid upstream node -=== TEST 30: get APISIX-NACOS info from NACOS - metadata filtering version=1.0 (only server1) +=== TEST 30: metadata filtering with multiple values - should match both servers (lane=a,b) --- yaml_config eval: $::yaml_config --- apisix_yaml routes: @@ -1194,21 +1194,41 @@ routes: type: roundrobin discovery_args: metadata: - version: "1.0" + lane: ["a", "b"] #END ---- pipelined_requests eval -[ - "GET /hello", - "GET /hello", - "GET /hello", - "GET /hello", - "GET /hello", -] ---- response_body_like eval -[ - qr/server 1/, - qr/server 1/, - qr/server 1/, - qr/server 1/, - qr/server 1/, -] +--- config + location /t { + content_by_lua_block { + local http = require("resty.http") + local uri = "http://127.0.0.1:" .. ngx.var.server_port + + -- Wait for 2 seconds for APISIX initialization + ngx.sleep(2) + local httpc = http.new() + + local server1_count = 0 + local server2_count = 0 + for i = 1, 10 do + local res, err = httpc:request_uri(uri .. "/hello", {method = "GET"}) + if res then + local clean_body = res.body:gsub("%s+$", "") + if clean_body == "server 1" then + server1_count = server1_count + 1 + elseif clean_body == "server 2" then + server2_count = server2_count + 1 + end + end + ngx.sleep(0.1) + end + + if server1_count > 0 and server2_count > 0 then + ngx.say("PASS") + else + ngx.say("FAIL") + end + } + } +--- request +GET /t +--- response_body +PASS From da59ef953a799a04d457a9633433095d69381aed Mon Sep 17 00:00:00 2001 From: qianz Date: Sun, 10 Aug 2025 13:55:31 +0800 Subject: [PATCH 07/13] lint: fix --- t/discovery/nacos.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/discovery/nacos.t b/t/discovery/nacos.t index 7f5b1e0e9504..030c7b65b5d8 100644 --- a/t/discovery/nacos.t +++ b/t/discovery/nacos.t @@ -1205,7 +1205,7 @@ routes: -- Wait for 2 seconds for APISIX initialization ngx.sleep(2) local httpc = http.new() - + local server1_count = 0 local server2_count = 0 for i = 1, 10 do From 7f2fc3dc8a963a68e3a5623b9cbc67f11a215919 Mon Sep 17 00:00:00 2001 From: qianz Date: Sun, 10 Aug 2025 14:15:18 +0800 Subject: [PATCH 08/13] feat: remove redundant check --- apisix/discovery/nacos/init.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua index 8a20f5f10fcd..64acf0c36051 100644 --- a/apisix/discovery/nacos/init.lua +++ b/apisix/discovery/nacos/init.lua @@ -57,9 +57,6 @@ end local function metadata_contains(host_metadata, route_metadata) - if not route_metadata or not next(route_metadata) then - return true - end if not host_metadata or not next(host_metadata) then return false end From 51ae1d46763f5ee455fe81f1b4d432568912353b Mon Sep 17 00:00:00 2001 From: qianz Date: Tue, 19 Aug 2025 00:04:49 +0800 Subject: [PATCH 09/13] doc: update nacos metadata match example --- docs/en/latest/discovery/nacos.md | 13 +++++++------ docs/zh/latest/discovery/nacos.md | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/en/latest/discovery/nacos.md b/docs/en/latest/discovery/nacos.md index 689ac4edbf6e..5ab5324056ed 100644 --- a/docs/en/latest/discovery/nacos.md +++ b/docs/en/latest/discovery/nacos.md @@ -282,9 +282,9 @@ The formatted response as below: #### Metadata filtering -APISIX supports filtering service instances based on metadata. When a route is configured with metadata conditions, only service instances whose metadata contains all the key-value pairs specified in the route's `metadata` configuration will be selected. +APISIX supports filtering service instances based on metadata. When a route is configured with metadata conditions, only service instances whose metadata contains all the key-value pairs specified in the route's `metadata` configuration will be selected. The metadata values in the route configuration are arrays, and a service instance matches if its metadata value equals any value in the corresponding array. -Example: If a service instance has metadata `{lane: "a", env: "prod", version: "1.0"}`, it will match routes configured with metadata `{lane: "a"}` or `{lane: "a", env: "prod"}`, but not routes configured with `{lane: "b"}` or `{lane: "a", region: "us"}`. +Example: If a service instance has metadata `{lane: "a", env: "prod", version: "1.0"}`, it will match routes configured with metadata `{lane: ["a"]}` or `{lane: ["a"], env: ["prod"]}`, but not routes configured with `{lane: ["b"]}` or `{lane: ["a"], region: ["us"]}`. Example of routing a request with metadata filtering: @@ -298,7 +298,7 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/5 -H "X-API-KEY: $admin_key" -X "discovery_type": "nacos", "discovery_args": { "metadata": { - "version": "v1" + "version": ["v1"] } } } @@ -319,12 +319,13 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/6 -H "X-API-KEY: $admin_key" -X "discovery_type": "nacos", "discovery_args": { "metadata": { - "lane": "a", - "env": "prod" + "lane": ["a", "b"], + "env": ["prod"] } } } }' ``` -This route will only route traffic to service instances that have both `lane: "a"` and `env: "prod"` in their metadata. +This route will only route traffic to service instances that have `env: "prod"` and `lane` set to either `"a"` or `"b"` in their metadata. + diff --git a/docs/zh/latest/discovery/nacos.md b/docs/zh/latest/discovery/nacos.md index b0fc35988c96..e3f7daa5fb1a 100644 --- a/docs/zh/latest/discovery/nacos.md +++ b/docs/zh/latest/discovery/nacos.md @@ -285,9 +285,9 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/4 -H "X-API-KEY: $admin_key" -X #### 使用元数据过滤服务实例 -APISIX 支持根据元数据过滤服务实例。当路由配置了元数据条件时,只有服务实例的元数据包含路由配置中指定的所有键值对,该服务实例才会被选中。 +APISIX 支持根据元数据过滤服务实例。当路由配置了元数据条件时,只有服务实例的元数据包含路由配置中指定的所有键值对,该服务实例才会被选中。路由配置中的元数据值为数组,如果服务实例的元数据值等于数组中的任意一个值,则匹配成功。 -举例:如果服务实例的元数据是 `{lane: "a", env: "prod", version: "1.0"}`,那么它能匹配配置了元数据 `{lane: "a"}` 或 `{lane: "a", env: "prod"}` 的路由,但不能匹配配置了 `{lane: "b"}` 或 `{lane: "a", region: "us"}` 的路由。 +举例:如果服务实例的元数据是 `{lane: "a", env: "prod", version: "1.0"}`,那么它能匹配配置了元数据 `{lane: ["a"]}` 或 `{lane: ["a"], env: ["prod"]}` 的路由,但不能匹配配置了 `{lane: ["b"]}` 或 `{lane: ["a"], region: ["us"]}` 的路由。 使用元数据过滤的路由配置示例: @@ -301,7 +301,7 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/5 -H "X-API-KEY: $admin_key" -X "discovery_type": "nacos", "discovery_args": { "metadata": { - "version": "v1" + "version": ["v1"] } } } @@ -322,12 +322,12 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/6 -H "X-API-KEY: $admin_key" -X "discovery_type": "nacos", "discovery_args": { "metadata": { - "lane": "a", - "env": "prod" + "lane": ["a", "b"], + "env": ["prod"] } } } }' ``` -此路由只会将流量转发到元数据中同时包含 `lane: "a"` 和 `env: "prod"` 的服务实例。 +此路由只会将流量转发到元数据中包含 `env: "prod"` 且 `lane` 为 `"a"` 或 `"b"` 的服务实例。 From 2bf849a02551fe6f888b27aff5159abfbda79890 Mon Sep 17 00:00:00 2001 From: qianz Date: Tue, 19 Aug 2025 00:10:51 +0800 Subject: [PATCH 10/13] feat: set uniqueItems for metadata array --- apisix/schema_def.lua | 3 ++- t/core/schema_def.t | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 6cb0944d96cf..1d823e8dd03f 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -495,7 +495,8 @@ local upstream_schema = { type = "array", items = { type = "string" - } + }, + uniqueItems = true } }, }, diff --git a/t/core/schema_def.t b/t/core/schema_def.t index 53ee3fb8a789..4270d61a5e7b 100644 --- a/t/core/schema_def.t +++ b/t/core/schema_def.t @@ -317,6 +317,21 @@ passed }, expected_error_pattern = "metadata.*wrong type" }, + { + name = "invalid metadata with duplicate values in array", + should_pass = false, + upstream = { + service_name = "test-service", + discovery_type = "nacos", + type = "roundrobin", + discovery_args = { + metadata = { + lane = {"a", "b", "a"} -- duplicate "a" should fail uniqueItems validation + } + } + }, + expected_error_pattern = "expected unique items" + }, } -- Execute all test cases From 6715ce9c12bb91ed0d6c5dc834c6bbfb2ad6e1a2 Mon Sep 17 00:00:00 2001 From: qianz Date: Tue, 19 Aug 2025 00:15:25 +0800 Subject: [PATCH 11/13] doc: update nacos metadata match example --- docs/en/latest/discovery/nacos.md | 2 +- docs/zh/latest/discovery/nacos.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/discovery/nacos.md b/docs/en/latest/discovery/nacos.md index 5ab5324056ed..e9335bed10ce 100644 --- a/docs/en/latest/discovery/nacos.md +++ b/docs/en/latest/discovery/nacos.md @@ -132,7 +132,7 @@ $ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY: $admin_ | ------------ | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | | namespace_id | string | optional | public | | This parameter is used to specify the namespace of the corresponding service | | group_name | string | optional | DEFAULT_GROUP | | This parameter is used to specify the group of the corresponding service | -| metadata | object | optional | {} | | Filter service instances by metadata using containment matching | +| metadata | object | optional | | | Filter service instances by metadata using containment matching | #### Specify the namespace diff --git a/docs/zh/latest/discovery/nacos.md b/docs/zh/latest/discovery/nacos.md index e3f7daa5fb1a..da94716e968e 100644 --- a/docs/zh/latest/discovery/nacos.md +++ b/docs/zh/latest/discovery/nacos.md @@ -132,7 +132,7 @@ $ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY: $admin_ | ------------ | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | | namespace_id | string | 可选 | public | | 服务所在的命名空间 | | group_name | string | 可选 | DEFAULT_GROUP | | 服务所在的组 | -| metadata | object | 可选 | {} | | 使用包含匹配方式根据元数据过滤服务实例 | +| metadata | object | 可选 | | | 使用包含匹配方式根据元数据过滤服务实例 | #### 指定命名空间 From 31755d88b989f31dc98bc7ba8037625077c4ccb6 Mon Sep 17 00:00:00 2001 From: qianz Date: Tue, 19 Aug 2025 00:39:52 +0800 Subject: [PATCH 12/13] feat: validate metadata type from nacos --- apisix/discovery/nacos/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua index 64acf0c36051..e1611d939121 100644 --- a/apisix/discovery/nacos/init.lua +++ b/apisix/discovery/nacos/init.lua @@ -23,6 +23,7 @@ local ipairs = ipairs local pairs = pairs local next = next local type = type +local assert = assert local math = math local math_random = math.random local ngx = ngx @@ -343,6 +344,7 @@ local function fetch_full_registry(premature) local key = get_key(namespace_id, group_name, service_info.service_name) service_names[key] = true for _, host in ipairs(data.hosts) do + assert(host.metadata == nil or type(host.metadata) == "table") local node = { host = host.ip, port = host.port, From 4952aa80c8ed62f90d91cb753e44a7b370c3126f Mon Sep 17 00:00:00 2001 From: qianz Date: Tue, 19 Aug 2025 00:53:08 +0800 Subject: [PATCH 13/13] doc: fix markdownlint error --- docs/en/latest/discovery/nacos.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/en/latest/discovery/nacos.md b/docs/en/latest/discovery/nacos.md index e9335bed10ce..707b731f2b73 100644 --- a/docs/en/latest/discovery/nacos.md +++ b/docs/en/latest/discovery/nacos.md @@ -328,4 +328,3 @@ $ curl http://127.0.0.1:9180/apisix/admin/routes/6 -H "X-API-KEY: $admin_key" -X ``` This route will only route traffic to service instances that have `env: "prod"` and `lane` set to either `"a"` or `"b"` in their metadata. -