You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by sp...@apache.org on 2022/12/07 06:03:40 UTC
[apisix] branch master updated: feat: add consul discovery module (#8380)
This is an automated email from the ASF dual-hosted git repository.
spacewander 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 04dbc61f9 feat: add consul discovery module (#8380)
04dbc61f9 is described below
commit 04dbc61f9447898f37c7ebd63dacd9c83c594c34
Author: Fabriceli <li...@gmail.com>
AuthorDate: Wed Dec 7 14:03:34 2022 +0800
feat: add consul discovery module (#8380)
---
Makefile | 3 +-
apisix/discovery/consul/init.lua | 416 ++++++++++++++++++++++++++
apisix/discovery/consul/schema.lua | 86 ++++++
ci/pod/docker-compose.first.yml | 4 +-
conf/config-default.yaml | 23 ++
docs/en/latest/config.json | 1 +
docs/en/latest/discovery/consul.md | 298 +++++++++++++++++++
t/discovery/consul.t | 578 +++++++++++++++++++++++++++++++++++++
t/discovery/consul_dump.t | 453 +++++++++++++++++++++++++++++
9 files changed, 1859 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
index 740eb2717..f5908f71f 100644
--- a/Makefile
+++ b/Makefile
@@ -283,7 +283,8 @@ install: runtime
$(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery
$(ENV_INSTALL) apisix/discovery/*.lua $(ENV_INST_LUADIR)/apisix/discovery/
- $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery/{consul_kv,dns,eureka,nacos,kubernetes,tars}
+ $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery/{consul,consul_kv,dns,eureka,nacos,kubernetes,tars}
+ $(ENV_INSTALL) apisix/discovery/consul/*.lua $(ENV_INST_LUADIR)/apisix/discovery/consul
$(ENV_INSTALL) apisix/discovery/consul_kv/*.lua $(ENV_INST_LUADIR)/apisix/discovery/consul_kv
$(ENV_INSTALL) apisix/discovery/dns/*.lua $(ENV_INST_LUADIR)/apisix/discovery/dns
$(ENV_INSTALL) apisix/discovery/eureka/*.lua $(ENV_INST_LUADIR)/apisix/discovery/eureka
diff --git a/apisix/discovery/consul/init.lua b/apisix/discovery/consul/init.lua
new file mode 100644
index 000000000..dd8275e7d
--- /dev/null
+++ b/apisix/discovery/consul/init.lua
@@ -0,0 +1,416 @@
+--
+-- 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 require = require
+local local_conf = require("apisix.core.config_local").local_conf()
+local core = require("apisix.core")
+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 ipairs = ipairs
+local error = error
+local ngx = ngx
+local unpack = unpack
+local tonumber = tonumber
+local pairs = pairs
+local ngx_timer_at = ngx.timer.at
+local ngx_timer_every = ngx.timer.every
+local log = core.log
+local json_delay_encode = core.json.delay_encode
+local ngx_worker_id = ngx.worker.id
+
+local all_services = core.table.new(0, 5)
+local default_service
+local default_weight
+local skip_service_map = core.table.new(0, 1)
+local dump_params
+
+local events
+local events_list
+local consul_services
+
+local _M = {
+ version = 0.1,
+}
+
+
+local function discovery_consul_callback(data, event, source, pid)
+ all_services = data
+ log.notice("update local variable all_services, event is: ", event,
+ "source: ", source, "server pid:", pid,
+ ", all services: ", json_delay_encode(all_services, true))
+end
+
+
+function _M.all_nodes()
+ return all_services
+end
+
+
+function _M.nodes(service_name)
+ 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 not resp_list then
+ log.error("fetch nodes failed by ", service_name, ", return default service")
+ return default_service and {default_service}
+ end
+
+ log.info("process id: ", ngx_worker_id(), ", all_services[", service_name, "] = ",
+ json_delay_encode(resp_list, true))
+
+ return resp_list
+end
+
+
+local function parse_instance(node)
+ local service_name, host, port = node.Service, node.Address, node.Port
+ -- if exist, skip special service name
+ if service_name and skip_service_map[service_name] then
+ return false
+ end
+ -- "" means metadata of the service
+ return true, host, tonumber(port), "", service_name
+end
+
+
+local function update_all_services(server_name_prefix, data)
+ local up_services = core.table.new(0, #data)
+ local weight = default_weight
+ for _, node in pairs(data) do
+ local succ, ip, port, metadata, server_name = parse_instance(node)
+ if succ then
+ local nodes = up_services[server_name]
+ if not nodes then
+ nodes = core.table.new(1, 0)
+ up_services[server_name] = nodes
+ end
+ core.table.insert(nodes, {
+ host = ip,
+ port = port,
+ weight = metadata and metadata.weight or weight,
+ })
+ end
+ end
+
+ -- clean old unused data
+ local old_services = consul_services[server_name_prefix] or {}
+ for k, _ in pairs(old_services) do
+ all_services[k] = nil
+ end
+ core.table.clear(old_services)
+
+ for k, v in pairs(up_services) do
+ all_services[k] = v
+ end
+ consul_services[server_name_prefix] = up_services
+
+ log.info("update all services: ", json_delay_encode(all_services, true))
+end
+
+
+local function read_dump_services()
+ local data, err = util.read_file(dump_params.path)
+ if not data then
+ log.error("read dump file get error: ", err)
+ return
+ end
+
+ log.info("read dump file: ", data)
+ data = util.trim(data)
+ if #data == 0 then
+ log.error("dump file is empty")
+ return
+ end
+
+ local entity, err = core.json.decode(data)
+ if not entity then
+ log.error("decoded dump data got error: ", err, ", file content: ", data)
+ return
+ end
+
+ if not entity.services or not entity.last_update then
+ log.warn("decoded dump data miss fields, file content: ", data)
+ return
+ end
+
+ local now_time = ngx.time()
+ log.info("dump file last_update: ", entity.last_update, ", dump_params.expire: ",
+ dump_params.expire, ", now_time: ", now_time)
+ if dump_params.expire ~= 0 and (entity.last_update + dump_params.expire) < now_time then
+ log.warn("dump file: ", dump_params.path, " had expired, ignored it")
+ return
+ end
+
+ all_services = entity.services
+ log.info("load dump file into memory success")
+end
+
+
+local function write_dump_services()
+ local entity = {
+ services = all_services,
+ last_update = ngx.time(),
+ expire = dump_params.expire, -- later need handle it
+ }
+ local data = core.json.encode(entity)
+ local succ, err = util.write_file(dump_params.path, data)
+ if not succ then
+ log.error("write dump into file got error: ", err)
+ end
+end
+
+
+local function show_dump_file()
+ if not dump_params then
+ return 503, "dump params is nil"
+ end
+
+ local data, err = util.read_file(dump_params.path)
+ if not data then
+ return 503, err
+ end
+
+ return 200, data
+end
+
+local function get_retry_delay(retry_delay)
+ if not retry_delay then
+ retry_delay = 1
+ else
+ retry_delay = retry_delay * 4
+ end
+
+ return retry_delay
+end
+
+
+function _M.connect(premature, consul_server, retry_delay)
+ if premature then
+ return
+ end
+
+ local consul_client = resty_consul:new({
+ host = consul_server.host,
+ port = consul_server.port,
+ connect_timeout = consul_server.connect_timeout,
+ read_timeout = consul_server.read_timeout,
+ default_args = consul_server.default_args,
+ })
+
+ log.info("consul_server: ", json_delay_encode(consul_server, true))
+ local watch_result, watch_err = consul_client:get(consul_server.consul_watch_sub_url)
+ local watch_error_info = (watch_err ~= nil and watch_err)
+ or ((watch_result ~= nil and watch_result.status ~= 200)
+ and watch_result.status)
+ if watch_error_info then
+ log.error("connect consul: ", consul_server.consul_server_url,
+ " by sub url: ", consul_server.consul_watch_sub_url,
+ ", got watch result: ", json_delay_encode(watch_result, true),
+ ", with error: ", watch_error_info)
+
+ retry_delay = get_retry_delay(retry_delay)
+ log.warn("retry connecting consul after ", retry_delay, " seconds")
+ core_sleep(retry_delay)
+
+ goto ERR
+ end
+
+ log.info("connect consul: ", consul_server.consul_server_url,
+ ", watch_result status: ", watch_result.status,
+ ", watch_result.headers.index: ", watch_result.headers['X-Consul-Index'],
+ ", consul_server.index: ", consul_server.index,
+ ", consul_server: ", json_delay_encode(consul_server, true))
+
+ -- if current index different last index then update service
+ if consul_server.index ~= watch_result.headers['X-Consul-Index'] then
+
+ -- fetch all services info
+ local result, err = consul_client:get(consul_server.consul_sub_url)
+
+ local error_info = (err ~= nil and err) or
+ ((result ~= nil and result.status ~= 200) and result.status)
+
+ if error_info then
+ log.error("connect consul: ", consul_server.consul_server_url,
+ " by sub url: ", consul_server.consul_sub_url,
+ ", got result: ", json_delay_encode(result, true),
+ ", with error: ", error_info)
+
+ retry_delay = get_retry_delay(retry_delay)
+ log.warn("retry connecting consul after ", retry_delay, " seconds")
+ core_sleep(retry_delay)
+
+ goto ERR
+ end
+
+ consul_server.index = watch_result.headers['X-Consul-Index']
+ -- only long connect type use index
+ if consul_server.keepalive then
+ consul_server.default_args.index = watch_result.headers['X-Consul-Index']
+ end
+ -- decode body, decode json, update service, error handling
+ if result.body then
+ log.notice("server_name: ", consul_server.consul_server_url,
+ ", header: ", json_delay_encode(result.headers, true),
+ ", body: ", json_delay_encode(result.body, true))
+ update_all_services(consul_server.consul_server_url, result.body)
+ --update events
+ local ok, err = events.post(events_list._source, events_list.updating, all_services)
+ if not ok then
+ log.error("post_event failure with ", events_list._source,
+ ", update all services error: ", err)
+ end
+
+ if dump_params then
+ ngx_timer_at(0, write_dump_services)
+ end
+ end
+ end
+
+ :: ERR ::
+ local keepalive = consul_server.keepalive
+ if keepalive then
+ local ok, err = ngx_timer_at(0, _M.connect, consul_server, retry_delay)
+ if not ok then
+ log.error("create ngx_timer_at got error: ", err)
+ return
+ end
+ end
+end
+
+
+local function format_consul_params(consul_conf)
+ local consul_server_list = core.table.new(0, #consul_conf.servers)
+ local args
+
+ if consul_conf.keepalive == false then
+ args = {}
+ elseif consul_conf.keepalive then
+ args = {
+ wait = consul_conf.timeout.wait, --blocked wait!=0; unblocked by wait=0
+ index = 0,
+ }
+ end
+
+ for _, v in pairs(consul_conf.servers) do
+ local scheme, host, port, path = unpack(http.parse_uri(nil, v))
+ if scheme ~= "http" then
+ return nil, "only support consul http schema address, eg: http://address:port"
+ elseif path ~= "/" or core.string.has_suffix(v, '/') then
+ return nil, "invalid consul server address, the valid format: http://address:port"
+ end
+
+ core.table.insert(consul_server_list, {
+ host = host,
+ port = port,
+ connect_timeout = consul_conf.timeout.connect,
+ read_timeout = consul_conf.timeout.read,
+ consul_sub_url = "/agent/services",
+ consul_watch_sub_url = "/catalog/services",
+ consul_server_url = v .. "/v1",
+ weight = consul_conf.weight,
+ keepalive = consul_conf.keepalive,
+ default_args = args,
+ index = 0,
+ fetch_interval = consul_conf.fetch_interval -- fetch interval to next connect consul
+ })
+ end
+
+ return consul_server_list, nil
+end
+
+
+function _M.init_worker()
+ local consul_conf = local_conf.discovery.consul
+
+ if consul_conf.dump then
+ local dump = consul_conf.dump
+ dump_params = dump
+
+ if dump.load_on_init then
+ read_dump_services()
+ end
+ end
+
+ events = require("resty.worker.events")
+ events_list = events.event_list(
+ "discovery_consul_update_all_services",
+ "updating"
+ )
+
+ if 0 ~= ngx_worker_id() then
+ events.register(discovery_consul_callback, events_list._source, events_list.updating)
+ return
+ end
+
+ log.notice("consul_conf: ", json_delay_encode(consul_conf, true))
+ default_weight = consul_conf.weight
+ -- set default service, used when the server node cannot be found
+ if consul_conf.default_service then
+ default_service = consul_conf.default_service
+ default_service.weight = default_weight
+ end
+ if consul_conf.skip_services then
+ skip_service_map = core.table.new(0, #consul_conf.skip_services)
+ for _, v in ipairs(consul_conf.skip_services) do
+ skip_service_map[v] = true
+ end
+ end
+
+ local consul_servers_list, err = format_consul_params(consul_conf)
+ if err then
+ error(err)
+ end
+ log.info("consul_server_list: ", json_delay_encode(consul_servers_list, true))
+
+ consul_services = core.table.new(0, 1)
+ -- success or failure
+ for _, server in ipairs(consul_servers_list) do
+ local ok, err = ngx_timer_at(0, _M.connect, server)
+ if not ok then
+ error("create consul got error: " .. err)
+ end
+
+ if server.keepalive == false then
+ ngx_timer_every(server.fetch_interval, _M.connect, server)
+ end
+ end
+end
+
+
+function _M.dump_data()
+ return {config = local_conf.discovery.consul, services = all_services }
+end
+
+
+function _M.control_api()
+ return {
+ {
+ methods = {"GET"},
+ uris = {"/show_dump_file"},
+ handler = show_dump_file,
+ }
+ }
+end
+
+
+return _M
diff --git a/apisix/discovery/consul/schema.lua b/apisix/discovery/consul/schema.lua
new file mode 100644
index 000000000..3e998b015
--- /dev/null
+++ b/apisix/discovery/consul/schema.lua
@@ -0,0 +1,86 @@
+--
+-- 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.
+--
+return {
+ type = "object",
+ properties = {
+ servers = {
+ type = "array",
+ minItems = 1,
+ items = {
+ type = "string",
+ }
+ },
+ fetch_interval = {type = "integer", minimum = 1, default = 3},
+ keepalive = {
+ type = "boolean",
+ default = true
+ },
+ weight = {type = "integer", minimum = 1, default = 1},
+ timeout = {
+ type = "object",
+ properties = {
+ connect = {type = "integer", minimum = 1, default = 2000},
+ read = {type = "integer", minimum = 1, default = 2000},
+ wait = {type = "integer", minimum = 1, default = 60}
+ },
+ default = {
+ connect = 2000,
+ read = 2000,
+ wait = 60,
+ }
+ },
+ skip_services = {
+ type = "array",
+ minItems = 1,
+ items = {
+ type = "string",
+ }
+ },
+ dump = {
+ type = "object",
+ properties = {
+ path = {type = "string", minLength = 1},
+ load_on_init = {type = "boolean", default = true},
+ expire = {type = "integer", default = 0},
+ },
+ required = {"path"},
+ },
+ default_service = {
+ type = "object",
+ properties = {
+ host = {type = "string"},
+ port = {type = "integer"},
+ metadata = {
+ type = "object",
+ properties = {
+ fail_timeout = {type = "integer", default = 1},
+ weight = {type = "integer", default = 1},
+ max_fails = {type = "integer", default = 1}
+ },
+ default = {
+ fail_timeout = 1,
+ weight = 1,
+ max_fails = 1
+ }
+ }
+ }
+ }
+ },
+
+ required = {"servers"}
+}
+
diff --git a/ci/pod/docker-compose.first.yml b/ci/pod/docker-compose.first.yml
index a13ad3cf1..62ef7a328 100644
--- a/ci/pod/docker-compose.first.yml
+++ b/ci/pod/docker-compose.first.yml
@@ -33,7 +33,7 @@ services:
restart: unless-stopped
ports:
- "8500:8500"
- command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data" ]
+ command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data", "-enable-script-checks" ]
networks:
consul_net:
@@ -42,7 +42,7 @@ services:
restart: unless-stopped
ports:
- "8600:8500"
- command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data" ]
+ command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data", "-enable-script-checks" ]
networks:
consul_net:
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index f14db8ef2..59a6de6b0 100755
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -316,6 +316,29 @@ nginx_config: # config for render the template to generate n
# dump: # if you need, when registered nodes updated can dump into file
# path: "logs/consul_kv.dump"
# expire: 2592000 # unit sec, here is 30 day
+# consul:
+# servers:
+# - "http://127.0.0.1:8500"
+# - "http://127.0.0.1:8600"
+# skip_services: # if you need to skip special services
+# - "service_a"
+# timeout:
+# connect: 2000 # default 2000 ms
+# read: 2000 # default 2000 ms
+# wait: 60 # default 60 sec
+# weight: 1 # default 1
+# fetch_interval: 3 # default 3 sec, only take effect for keepalive: false way
+# keepalive: true # default true, use the long pull way to query consul servers
+# default_service: # you can define default server when missing hit
+# host: "127.0.0.1"
+# port: 20999
+# metadata:
+# fail_timeout: 1 # default 1 ms
+# weight: 1 # default 1
+# max_fails: 1 # default 1
+# dump: # if you need, when registered nodes updated can dump into file
+# path: "logs/consul.dump"
+# expire: 2592000 # unit sec, here is 30 day
# kubernetes:
# ### kubernetes service discovery both support single-cluster and multi-cluster mode
# ### applicable to the case where the service is distributed in a single or multiple kubernetes clusters.
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index b5dd26e1d..73836ab41 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -267,6 +267,7 @@
"items": [
"discovery",
"discovery/dns",
+ "discovery/consul",
"discovery/consul_kv",
"discovery/nacos",
"discovery/eureka",
diff --git a/docs/en/latest/discovery/consul.md b/docs/en/latest/discovery/consul.md
new file mode 100644
index 000000000..17e13b001
--- /dev/null
+++ b/docs/en/latest/discovery/consul.md
@@ -0,0 +1,298 @@
+---
+title: consul
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Summary
+
+APACHE APISIX supports Consul as a service discovery
+
+## Configuration for discovery client
+
+### Configuration for Consul
+
+First of all, we need to add following configuration in `conf/config.yaml` :
+
+```yaml
+discovery:
+ consul:
+ servers: # make sure service name is unique in these consul servers
+ - "http://127.0.0.1:8500"
+ - "http://127.0.0.1:8600" # `http://127.0.0.1:8500` and `http://127.0.0.1:8600` are different clusters
+ skip_services: # if you need to skip special services
+ - "service_a"
+ timeout:
+ connect: 1000 # default 2000 ms
+ read: 1000 # default 2000 ms
+ wait: 60 # default 60 sec
+ weight: 1 # default 1
+ fetch_interval: 5 # default 3 sec, only take effect for keepalive: false way
+ keepalive: true # default true, use the long pull way to query consul servers
+ default_service: # you can define default service when missing hit
+ host: "127.0.0.1"
+ port: 20999
+ metadata:
+ fail_timeout: 1 # default 1 ms
+ weight: 1 # default 1
+ max_fails: 1 # default 1
+ dump: # if you need, when registered nodes updated can dump into file
+ path: "logs/consul.dump"
+ expire: 2592000 # unit sec, here is 30 day
+```
+
+And you can config it in short by default value:
+
+```yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+```
+
+The `keepalive` has two optional values:
+
+- `true`, default and recommend value, use the long pull way to query consul servers
+- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval
+
+#### Dump Data
+
+When we need reload `apisix` online, as the `consul` module maybe loads data from CONSUL slower than load routes from ETCD, and would get the log at the moment before load successfully from consul:
+
+```
+ http_access_phase(): failed to set upstream: no valid upstream node
+```
+
+So, we import the `dump` function for `consul` module. When reload, would load the dump file before from consul; when the registered nodes in consul been updated, would dump the upstream nodes into file automatically.
+
+The `dump` has three optional values now:
+
+- `path`, the dump file save path
+ - support relative path, eg: `logs/consul.dump`
+ - support absolute path, eg: `/tmp/consul.dump`
+ - make sure the dump file's parent path exist
+ - make sure the `apisix` has the dump file's read-write access permission,eg: add below config in `conf/config.yaml`
+
+```yaml
+nginx_config: # config for render the template to generate nginx.conf
+ user: root # specifies the execution user of the worker process.
+```
+
+- `load_on_init`, default value is `true`
+ - if `true`, just try to load the data from the dump file before loading data from consul when starting, does not care the dump file exists or not
+ - if `false`, ignore loading data from the dump file
+ - Whether `true` or `false`, we don't need to prepare a dump file for apisix at anytime
+- `expire`, unit sec, avoiding load expired dump data when load
+ - default `0`, it is unexpired forever
+ - recommend 2592000, which is 30 days(equals 3600 \* 24 \* 30)
+
+### Register Http API Services
+
+Now, register nodes into consul:
+
+```shell
+curl -X PUT 'http://127.0.0.1:8500/v1/agent/service/register' \
+-d '{
+ "ID": "service_a1",
+ "Name": "service_a",
+ "Tags": ["primary", "v1"],
+ "Address": "127.0.0.1",
+ "Port": 8000,
+ "Meta": {
+ "service_a_version": "4.0"
+ },
+ "EnableTagOverride": false,
+ "Weights": {
+ "Passing": 10,
+ "Warning": 1
+ }
+}'
+
+curl -X PUT 'http://127.0.0.1:8500/v1/agent/service/register' \
+-d '{
+ "ID": "service_a1",
+ "Name": "service_a",
+ "Tags": ["primary", "v1"],
+ "Address": "127.0.0.1",
+ "Port": 8002,
+ "Meta": {
+ "service_a_version": "4.0"
+ },
+ "EnableTagOverride": false,
+ "Weights": {
+ "Passing": 10,
+ "Warning": 1
+ }
+}'
+```
+
+In some case, same service name exist in different consul servers.
+To avoid confusion, use the full consul key url path as service name in practice.
+
+### Upstream setting
+
+Here is an example of routing a request with a URL of "/*" to a service which named "service_a" and use consul discovery client in the registry :
+
+```shell
+$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
+{
+ "uri": "/*",
+ "upstream": {
+ "service_name": "service_a",
+ "type": "roundrobin",
+ "discovery_type": "consul"
+ }
+}'
+```
+
+The format response as below:
+
+```json
+{
+ "key": "/apisix/routes/1",
+ "value": {
+ "uri": "/*",
+ "priority": 0,
+ "id": "1",
+ "upstream": {
+ "scheme": "http",
+ "type": "roundrobin",
+ "hash_on": "vars",
+ "discovery_type": "consul",
+ "service_name": "service_a",
+ "pass_host": "pass"
+ },
+ "create_time": 1669267329,
+ "status": 1,
+ "update_time": 1669267329
+ }
+}
+```
+
+You could find more usage in the `apisix/t/discovery/consul.t` file.
+
+## Debugging API
+
+It also offers control api for debugging.
+
+### Memory Dump API
+
+```shell
+GET /v1/discovery/consul/dump
+```
+
+For example:
+
+```shell
+# curl http://127.0.0.1:9090/v1/discovery/consul/dump | jq
+{
+ "config": {
+ "fetch_interval": 3,
+ "timeout": {
+ "wait": 60,
+ "connect": 6000,
+ "read": 6000
+ },
+ "weight": 1,
+ "servers": [
+ "http://172.19.5.30:8500",
+ "http://172.19.5.31:8500"
+ ],
+ "keepalive": true,
+ "default_service": {
+ "host": "172.19.5.11",
+ "port": 8899,
+ "metadata": {
+ "fail_timeout": 1,
+ "weight": 1,
+ "max_fails": 1
+ }
+ },
+ "skip_services": [
+ "service_d"
+ ]
+ },
+ "services": {
+ "service_a": [
+ {
+ "host": "127.0.0.1",
+ "port": 30513,
+ "weight": 1
+ },
+ {
+ "host": "127.0.0.1",
+ "port": 30514,
+ "weight": 1
+ }
+ ],
+ "service_b": [
+ {
+ "host": "172.19.5.51",
+ "port": 50051,
+ "weight": 1
+ }
+ ],
+ "service_c": [
+ {
+ "host": "127.0.0.1",
+ "port": 30511,
+ "weight": 1
+ },
+ {
+ "host": "127.0.0.1",
+ "port": 30512,
+ "weight": 1
+ }
+ ]
+ }
+}
+```
+
+### Show Dump File API
+
+It offers another control api for dump file view now. Maybe would add more api for debugging in future.
+
+```shell
+GET /v1/discovery/consul/show_dump_file
+```
+
+For example:
+
+```shell
+curl http://127.0.0.1:9090/v1/discovery/consul/show_dump_file | jq
+{
+ "services": {
+ "service_a": [
+ {
+ "host": "172.19.5.12",
+ "port": 8000,
+ "weight": 120
+ },
+ {
+ "host": "172.19.5.13",
+ "port": 8000,
+ "weight": 120
+ }
+ ]
+ },
+ "expire": 0,
+ "last_update": 1615877468
+}
+```
diff --git a/t/discovery/consul.t b/t/discovery/consul.t
new file mode 100644
index 000000000..39c5ab287
--- /dev/null
+++ b/t/discovery/consul.t
@@ -0,0 +1,578 @@
+#
+# 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);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $http_config = $block->http_config // <<_EOC_;
+
+ server {
+ listen 20999;
+
+ location / {
+ content_by_lua_block {
+ ngx.say("missing consul services")
+ }
+ }
+ }
+
+ server {
+ listen 30511;
+
+ location /hello {
+ content_by_lua_block {
+ ngx.say("server 1")
+ }
+ }
+ }
+ server {
+ listen 30512;
+
+ location /hello {
+ content_by_lua_block {
+ ngx.say("server 2")
+ }
+ }
+ }
+ server {
+ listen 30513;
+
+ location /hello {
+ content_by_lua_block {
+ ngx.say("server 3")
+ }
+ }
+ }
+ server {
+ listen 30514;
+
+ location /hello {
+ content_by_lua_block {
+ ngx.say("server 4")
+ }
+ }
+ }
+_EOC_
+
+ $block->set_value("http_config", $http_config);
+});
+
+our $yaml_config = <<_EOC_;
+apisix:
+ node_listen: 1984
+ enable_control: true
+ control:
+ ip: 127.0.0.1
+ port: 9090
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+ - "http://127.0.0.1:8600"
+ skip_services:
+ - "service_c"
+ timeout:
+ connect: 1000
+ read: 1000
+ wait: 60
+ weight: 1
+ fetch_interval: 1
+ keepalive: true
+ default_service:
+ host: "127.0.0.1"
+ port: 20999
+ metadata:
+ fail_timeout: 1
+ weight: 1
+ max_fails: 1
+_EOC_
+
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: prepare consul catalog register nodes
+--- config
+location /consul1 {
+ rewrite ^/consul1/(.*) /v1/agent/service/$1 break;
+ proxy_pass http://127.0.0.1:8500;
+}
+
+location /consul2 {
+ rewrite ^/consul2/(.*) /v1/agent/service/$1 break;
+ proxy_pass http://127.0.0.1:8600;
+}
+--- pipelined_requests eval
+[
+ "PUT /consul1/deregister/service_a1",
+ "PUT /consul1/deregister/service_b1",
+ "PUT /consul1/deregister/service_a2",
+ "PUT /consul1/deregister/service_b2",
+ "PUT /consul2/deregister/service_a1",
+ "PUT /consul2/deregister/service_b1",
+ "PUT /consul2/deregister/service_a2",
+ "PUT /consul2/deregister/service_b2",
+ "PUT /consul1/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /consul1/register\n" . "{\"ID\":\"service_a2\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30512,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /consul1/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30513,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /consul1/register\n" . "{\"ID\":\"service_b2\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30514,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- error_code eval
+[200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 2: test consul server 1
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- pipelined_requests eval
+[
+ "GET /hello",
+ "GET /hello",
+]
+--- response_body_like eval
+[
+ qr/server [1-2]\n/,
+ qr/server [1-2]\n/,
+]
+--- no_error_log
+[error, error]
+
+
+
+=== TEST 3: test consul server 2
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_b
+ discovery_type: consul
+ type: roundrobin
+#END
+--- pipelined_requests eval
+[
+ "GET /hello",
+ "GET /hello"
+]
+--- response_body_like eval
+[
+ qr/server [3-4]\n/,
+ qr/server [3-4]\n/,
+]
+--- no_error_log
+[error, error]
+
+
+
+=== TEST 4: test mini consul config
+--- yaml_config
+apisix:
+ node_listen: 1984
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+ - "http://127.0.0.1:6500"
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /hello
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body_like eval
+qr/server [1-2]/
+--- ignore_error_log
+
+
+
+=== TEST 5: test invalid service name sometimes the consul key maybe deleted by mistake
+
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_c
+ discovery_type: consul
+ type: roundrobin
+#END
+--- pipelined_requests eval
+[
+ "GET /hello_api",
+ "GET /hello_api"
+]
+--- response_body eval
+[
+ "missing consul services\n",
+ "missing consul services\n"
+]
+--- ignore_error_log
+
+
+
+=== TEST 6: test skip keys
+skip some services, return default nodes, get response: missing consul services
+--- yaml_config
+apisix:
+ node_listen: 1984
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8600"
+ prefix: "upstreams"
+ skip_services:
+ - "service_a"
+ default_service:
+ host: "127.0.0.1"
+ port: 20999
+ metadata:
+ fail_timeout: 1
+ weight: 1
+ max_fails: 1
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body eval
+"missing consul services\n"
+--- ignore_error_log
+
+
+
+=== TEST 7: test register and unregister nodes
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- config
+location /v1/agent {
+ proxy_pass http://127.0.0.1:8500;
+}
+location /sleep {
+ content_by_lua_block {
+ local args = ngx.req.get_uri_args()
+ local sec = args.sec or "2"
+ ngx.sleep(tonumber(sec))
+ ngx.say("ok")
+ }
+}
+--- timeout: 6
+--- request eval
+[
+ "PUT /v1/agent/service/deregister/service_a1",
+ "PUT /v1/agent/service/deregister/service_a2",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30513,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a2\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30514,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "GET /sleep",
+
+ "GET /hello?random1",
+ "GET /hello?random2",
+ "GET /hello?random3",
+ "GET /hello?random4",
+
+ "PUT /v1/agent/service/deregister/service_a1",
+ "PUT /v1/agent/service/deregister/service_a2",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a2\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30512,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "GET /sleep?sec=5",
+
+ "GET /hello?random1",
+ "GET /hello?random2",
+ "GET /hello?random3",
+ "GET /hello?random4",
+
+]
+--- response_body_like eval
+[
+ qr//,
+ qr//,
+ qr//,
+ qr//,
+ qr/ok\n/,
+
+ qr/server [3-4]\n/,
+ qr/server [3-4]\n/,
+ qr/server [3-4]\n/,
+ qr/server [3-4]\n/,
+
+ qr//,
+ qr//,
+ qr//,
+ qr//,
+ qr/ok\n/,
+
+ qr/server [1-2]\n/,
+ qr/server [1-2]\n/,
+ qr/server [1-2]\n/,
+ qr/server [1-2]\n/
+]
+--- ignore_error_log
+
+
+
+=== TEST 8: clean nodes
+--- config
+location /v1/agent {
+ proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+ "PUT /v1/agent/service/deregister/service_a1",
+ "PUT /v1/agent/service/deregister/service_a2",
+]
+--- error_code eval
+[200, 200]
+
+
+
+=== TEST 9: test consul short connect type
+--- yaml_config
+apisix:
+ node_listen: 1984
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+ keepalive: false
+ fetch_interval: 3
+ default_service:
+ host: "127.0.0.1"
+ port: 20999
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- config
+location /v1/agent {
+ proxy_pass http://127.0.0.1:8500;
+}
+location /sleep {
+ content_by_lua_block {
+ local args = ngx.req.get_uri_args()
+ local sec = args.sec or "2"
+ ngx.sleep(tonumber(sec))
+ ngx.say("ok")
+ }
+}
+--- timeout: 6
+--- request eval
+[
+ "GET /hello",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "GET /sleep?sec=5",
+ "GET /hello",
+]
+--- response_body_like eval
+[
+ qr/missing consul services\n/,
+ qr//,
+ qr/ok\n/,
+ qr/server 1\n/
+]
+--- ignore_error_log
+
+
+
+=== TEST 10: retry when Consul can't be reached (long connect type)
+--- yaml_config
+apisix:
+ node_listen: 1984
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8501"
+ keepalive: true
+ fetch_interval: 3
+ default_service:
+ host: "127.0.0.1"
+ port: 20999
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- timeout: 4
+--- config
+location /sleep {
+ content_by_lua_block {
+ local args = ngx.req.get_uri_args()
+ local sec = args.sec or "2"
+ ngx.sleep(tonumber(sec))
+ ngx.say("ok")
+ }
+}
+--- request
+GET /sleep?sec=3
+--- response_body
+ok
+--- grep_error_log eval
+qr/retry connecting consul after \d seconds/
+--- grep_error_log_out
+retry connecting consul after 1 seconds
+retry connecting consul after 4 seconds
+
+
+
+=== TEST 11: prepare healthy and unhealthy nodes
+--- config
+location /v1/agent {
+ proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+ "PUT /v1/agent/service/deregister/service_a1",
+ "PUT /v1/agent/service/deregister/service_a2",
+ "PUT /v1/agent/service/deregister/service_b1",
+ "PUT /v1/agent/service/deregister/service_b2",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30513,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_b2\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30514,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- error_code eval
+[200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 12: test health checker
+--- yaml_config eval: $::yaml_config
+--- apisix_yaml
+routes:
+ -
+ uris:
+ - /hello
+ upstream_id: 1
+upstreams:
+ -
+ service_name: service_b
+ discovery_type: consul
+ type: roundrobin
+ id: 1
+ checks:
+ active:
+ http_path: "/hello"
+ healthy:
+ interval: 1
+ successes: 1
+ unhealthy:
+ interval: 1
+ http_failures: 1
+#END
+--- config
+ location /thc {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin")
+ local http = require "resty.http"
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello"
+ local httpc = http.new()
+ httpc:request_uri(uri, {method = "GET"})
+ ngx.sleep(3)
+
+ local code, body, res = t.test('/v1/healthcheck',
+ ngx.HTTP_GET)
+ res = json.decode(res)
+ local nodes = res[1].nodes
+ table.sort(nodes, function(a, b)
+ return a.port < b.port
+ end)
+ ngx.say(json.encode(nodes))
+
+ local code, body, res = t.test('/v1/healthcheck/upstreams/1',
+ ngx.HTTP_GET)
+ res = json.decode(res)
+ nodes = res.nodes
+ table.sort(nodes, function(a, b)
+ return a.port < b.port
+ end)
+ ngx.say(json.encode(nodes))
+ }
+ }
+--- request
+GET /thc
+--- response_body
+[{"host":"127.0.0.1","port":30513,"priority":0,"weight":1},{"host":"127.0.0.1","port":30514,"priority":0,"weight":1}]
+[{"host":"127.0.0.1","port":30513,"priority":0,"weight":1},{"host":"127.0.0.1","port":30514,"priority":0,"weight":1}]
+--- ignore_error_log
diff --git a/t/discovery/consul_dump.t b/t/discovery/consul_dump.t
new file mode 100644
index 000000000..b229fb7a4
--- /dev/null
+++ b/t/discovery/consul_dump.t
@@ -0,0 +1,453 @@
+#
+# 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");
+
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ my $http_config = $block->http_config // <<_EOC_;
+
+ server {
+ listen 30511;
+
+ location /hello {
+ content_by_lua_block {
+ ngx.say("server 1")
+ }
+ }
+ }
+_EOC_
+
+ $block->set_value("http_config", $http_config);
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: prepare nodes
+--- config
+location /v1/agent {
+ proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+ "PUT /v1/agent/service/deregister/service_a1",
+ "PUT /v1/agent/service/deregister/service_a2",
+ "PUT /v1/agent/service/deregister/service_b1",
+ "PUT /v1/agent/service/deregister/service_b2",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /v1/agent/service/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":8002,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- response_body eval
+--- error_code eval
+[200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 2: show dump services
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+ dump:
+ path: "consul.dump"
+ load_on_init: true
+--- config
+ location /t {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin")
+ ngx.sleep(2)
+
+ local code, body, res = t.test('/v1/discovery/consul/show_dump_file',
+ ngx.HTTP_GET)
+ local entity = json.decode(res)
+ ngx.say(json.encode(entity.services))
+ }
+ }
+--- timeout: 3
+--- request
+GET /t
+--- response_body
+{"service_a":[{"host":"127.0.0.1","port":30511,"weight":1}],"service_b":[{"host":"127.0.0.1","port":8002,"weight":1}]}
+
+
+
+=== TEST 3: prepare dump file for next test
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+ dump:
+ path: "/tmp/consul.dump"
+ load_on_init: false
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body
+server 1
+
+
+
+=== TEST 4: clean registered nodes
+--- config
+location /v1/agent {
+ proxy_pass http://127.0.0.1:8500;
+}
+--- request eval
+[
+ "PUT /v1/agent/service/deregister/service_a1",
+ "PUT /v1/agent/service/deregister/service_b1",
+]
+--- error_code eval
+[200, 200]
+
+
+
+=== TEST 5: test load dump on init
+Configure the invalid consul server addr, and loading the last test 3 generated /tmp/consul.dump file into memory when initializing
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:38500"
+ dump:
+ path: "/tmp/consul.dump"
+ load_on_init: true
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body
+server 1
+--- error_log
+connect consul
+
+
+
+=== TEST 6: delete dump file
+--- config
+ location /t {
+ content_by_lua_block {
+ local util = require("apisix.cli.util")
+ local succ, err = util.execute_cmd("rm -f /tmp/consul.dump")
+ ngx.say(succ and "success" or err)
+ }
+ }
+--- request
+GET /t
+--- response_body
+success
+
+
+
+=== TEST 7: miss load dump on init
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:38500"
+ dump:
+ path: "/tmp/consul.dump"
+ load_on_init: true
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- error_code: 503
+--- error_log
+connect consul
+fetch nodes failed
+failed to set upstream
+
+
+
+=== TEST 8: prepare expired dump file
+--- config
+ location /t {
+ content_by_lua_block {
+ local util = require("apisix.cli.util")
+ local json = require("toolkit.json")
+
+ local applications = json.decode('{"service_a":[{"host":"127.0.0.1","port":30511,"weight":1}]}')
+ local entity = {
+ services = applications,
+ last_update = ngx.time(),
+ expire = 10,
+ }
+ local succ, err = util.write_file("/tmp/consul.dump", json.encode(entity))
+
+ ngx.sleep(2)
+ ngx.say(succ and "success" or err)
+ }
+ }
+--- timeout: 3
+--- request
+GET /t
+--- response_body
+success
+
+
+
+=== TEST 9: unexpired dump
+test load unexpired /tmp/consul.dump file generated by upper test when initializing
+ when initializing
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:38500"
+ dump:
+ path: "/tmp/consul.dump"
+ load_on_init: true
+ expire: 5
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- response_body
+server 1
+--- error_log
+connect consul
+
+
+
+=== TEST 10: expired dump
+test load expired ( by check: (dump_file.last_update + dump.expire) < ngx.time ) ) /tmp/consul.dump file generated by upper test when initializing
+ when initializing
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:38500"
+ dump:
+ path: "/tmp/consul.dump"
+ load_on_init: true
+ expire: 1
+#END
+--- apisix_yaml
+routes:
+ -
+ uri: /*
+ upstream:
+ service_name: service_a
+ discovery_type: consul
+ type: roundrobin
+#END
+--- request
+GET /hello
+--- error_code: 503
+--- error_log
+dump file: /tmp/consul.dump had expired, ignored it
+
+
+
+=== TEST 11: delete dump file
+--- config
+ location /t {
+ content_by_lua_block {
+ local util = require("apisix.cli.util")
+ local succ, err = util.execute_cmd("rm -f /tmp/consul.dump")
+ ngx.say(succ and "success" or err)
+ }
+ }
+--- request
+GET /t
+--- response_body
+success
+
+
+
+=== TEST 12: dump file inexistence
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:38500"
+ dump:
+ path: "/tmp/consul.dump"
+#END
+--- request
+GET /v1/discovery/consul/show_dump_file
+--- error_code: 503
+--- error_log
+connect consul
+
+
+
+=== TEST 13: no dump config
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:38500"
+#END
+--- request
+GET /v1/discovery/consul/show_dump_file
+--- error_code: 503
+--- error_log
+connect consul
+
+
+
+=== TEST 14: prepare nodes with different consul clusters
+--- config
+location /consul1 {
+ rewrite ^/consul1/(.*) /v1/agent/service/$1 break;
+ proxy_pass http://127.0.0.1:8500;
+}
+
+location /consul2 {
+ rewrite ^/consul2/(.*) /v1/agent/service/$1 break;
+ proxy_pass http://127.0.0.1:8600;
+}
+--- pipelined_requests eval
+[
+ "PUT /consul1/deregister/service_a1",
+ "PUT /consul1/deregister/service_b1",
+ "PUT /consul1/deregister/service_a2",
+ "PUT /consul1/deregister/service_b2",
+ "PUT /consul2/deregister/service_a1",
+ "PUT /consul2/deregister/service_b1",
+ "PUT /consul2/deregister/service_a2",
+ "PUT /consul2/deregister/service_b2",
+ "PUT /consul1/register\n" . "{\"ID\":\"service_a1\",\"Name\":\"service_a\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30511,\"Meta\":{\"service_a_version\":\"4.0\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+ "PUT /consul2/register\n" . "{\"ID\":\"service_b1\",\"Name\":\"service_b\",\"Tags\":[\"primary\",\"v1\"],\"Address\":\"127.0.0.1\",\"Port\":30517,\"Meta\":{\"service_b_version\":\"4.1\"},\"EnableTagOverride\":false,\"Weights\":{\"Passing\":10,\"Warning\":1}}",
+]
+--- error_code eval
+[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]
+
+
+
+=== TEST 15: show dump services with different consul clusters
+--- yaml_config
+apisix:
+ node_listen: 1984
+ enable_control: true
+discovery:
+ consul:
+ servers:
+ - "http://127.0.0.1:8500"
+ - "http://127.0.0.1:8600"
+ dump:
+ path: "consul.dump"
+ load_on_init: false
+--- config
+ location /bonjour {
+ content_by_lua_block {
+ local json = require("toolkit.json")
+ local t = require("lib.test_admin")
+ ngx.sleep(2)
+
+ local code, body, res = t.test('/v1/discovery/consul/show_dump_file',
+ ngx.HTTP_GET)
+ local entity = json.decode(res)
+ ngx.say(json.encode(entity.services))
+ }
+ }
+--- timeout: 3
+--- request
+GET /bonjour
+--- response_body
+{"service_a":[{"host":"127.0.0.1","port":30511,"weight":1}],"service_b":[{"host":"127.0.0.1","port":30517,"weight":1}]}