Skip to content

feat(consul): filter nodes in upstream with metadata #12448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions apisix/discovery/consul/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ local core_sleep = require("apisix.core.utils").sleep
local resty_consul = require('resty.consul')
local http = require('resty.http')
local util = require("apisix.cli.util")
local discovery_utils = require("apisix.utils.discovery")
local ipairs = ipairs
local error = error
local ngx = ngx
Expand Down Expand Up @@ -78,15 +79,18 @@ function _M.all_nodes()
return all_services
end


function _M.nodes(service_name)
function _M.nodes(service_name, discovery_args)
if not all_services then
log.error("all_services is nil, failed to fetch nodes for : ", service_name)
return
end

local resp_list = all_services[service_name]

if discovery_args and discovery_args.metadata_match then
resp_list = discovery_utils.nodes_metadata_match(resp_list, discovery_args.metadata_match)
end

if not resp_list then
log.error("fetch nodes failed by ", service_name, ", return default service")
return default_service and {default_service}
Expand All @@ -98,7 +102,6 @@ function _M.nodes(service_name)
return resp_list
end


local function update_all_services(consul_server_url, up_services)
-- clean old unused data
local old_services = consul_services[consul_server_url] or {}
Expand Down Expand Up @@ -511,11 +514,17 @@ function _M.connect(premature, consul_server, retry_delay)
local nodes = up_services[service_name]
local nodes_uniq = {}
for _, node in ipairs(result.body) do
if not node.Service then
local service = node.Service
if not service then
goto CONTINUE
end

local svc_address, svc_port = node.Service.Address, node.Service.Port
local svc_address = service.Address
local svc_port = service.Port
local metadata = service.Meta
if type(metadata) ~= "table" then
metadata = nil
end
-- Handle nil or 0 port case - default to 80 for HTTP services
if not svc_port or svc_port == 0 then
svc_port = 80
Expand All @@ -532,7 +541,8 @@ function _M.connect(premature, consul_server, retry_delay)
core.table.insert(nodes, {
host = svc_address,
port = tonumber(svc_port),
weight = default_weight,
weight = metadata and tonumber(metadata.weight) or default_weight,
metadata = metadata
})
nodes_uniq[service_id] = true
end
Expand Down
12 changes: 12 additions & 0 deletions apisix/schema_def.lua
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,18 @@ local upstream_schema = {
description = "group name",
type = "string",
},
metadata_match = {
description = "metadata for filtering service instances",
type = "object",
additionalProperties = {
type = "array",
items = {
description = "candidate metadata value",
type = "string",
},
uniqueItems = true,
}
},
}
},
pass_host = {
Expand Down
55 changes: 55 additions & 0 deletions apisix/utils/discovery.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--
-- 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.
--
local core = require("apisix.core")
local ipairs = ipairs
local pairs = pairs

local _M = {}

local function do_metadata_match(inst, filters)
local metadata = inst.metadata or {}
for key, values in pairs(filters) do
local matched = false
for _, value in ipairs(values) do
if metadata[key] == value then
matched = true
break
end
end
if not matched then
return false
end
end
return true
end

local function nodes_metadata_match(nodes, metadata_match)
if not nodes then
return nil
end

local result = {}
for _, node in ipairs(nodes) do
if do_metadata_match(node, metadata_match) then
core.table.insert(result, node)
end
end
return result
end
_M.nodes_metadata_match = nodes_metadata_match

return _M
55 changes: 55 additions & 0 deletions docs/en/latest/discovery/consul.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,61 @@ $ curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 -H "X-API-KEY: $admin_
}'
```

### discovery_args

| Name | Type | Requirement | Default | Valid | Description |
|----------------| ------ | ----------- | ------- | ----- | ------------------------------------------------------------ |
| metadata_match | object | optional | {} | | Filter service instances by metadata using containment matching |

#### Metadata filtering

APISIX supports filtering service instances based on metadata. When a route is configured with metadata conditions, only service instances whose metadata matched with roles specified in the route's `metadata_match` 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", "b"], env: "prod"}`, but not routes configured with `{lane: ["c"]}` 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", "v2"]
}
}
}
}'
```

This route will only route traffic to service instances that have the metadata field `version` set to `v1` or `v2`.

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.

You could find more usage in the `apisix/t/discovery/stream/consul.t` file.

## Debugging API
Expand Down
1 change: 1 addition & 0 deletions docs/zh/latest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
"items": [
"discovery",
"discovery/dns",
"discovery/consul",
"discovery/nacos",
"discovery/eureka",
"discovery/control-plane-service-discovery",
Expand Down
Loading
Loading