feat: add token support for consul discovery (#10278)

diff --git a/apisix/discovery/consul/init.lua b/apisix/discovery/consul/init.lua
index ae1e4c6..253d0cb 100644
--- a/apisix/discovery/consul/init.lua
+++ b/apisix/discovery/consul/init.lua
@@ -197,21 +197,20 @@
         port = consul_server.port,
         connect_timeout = consul_server.connect_timeout,
         read_timeout = consul_server.read_timeout,
+        default_args = {
+            token = consul_server.token,
+        }
     }
     if not consul_server.keepalive then
         return opts
     end
 
+    opts.default_args.wait = consul_server.wait_timeout --blocked wait!=0; unblocked by wait=0
+
     if is_catalog then
-        opts.default_args = {
-            wait = consul_server.wait_timeout, --blocked wait!=0; unblocked by wait=0
-            index = consul_server.catalog_index,
-        }
+        opts.default_args.index = consul_server.catalog_index
     else
-        opts.default_args = {
-            wait = consul_server.wait_timeout, --blocked wait!=0; unblocked by wait=0
-            index = consul_server.health_index,
-        }
+        opts.default_args.index = consul_server.health_index
     end
 
     return opts
@@ -396,6 +395,9 @@
         port = consul_server.port,
         connect_timeout = consul_server.connect_timeout,
         read_timeout = consul_server.read_timeout,
+        default_args = {
+            token = consul_server.token
+        }
     })
     local catalog_success, catalog_res, catalog_err = pcall(function()
         return consul_client:get(consul_server.consul_watch_catalog_url)
@@ -545,6 +547,7 @@
         core.table.insert(consul_server_list, {
             host = host,
             port = port,
+            token = consul_conf.token,
             connect_timeout = consul_conf.timeout.connect,
             read_timeout = consul_conf.timeout.read,
             wait_timeout = consul_conf.timeout.wait,
diff --git a/apisix/discovery/consul/schema.lua b/apisix/discovery/consul/schema.lua
index 3e998b0..d7cf295 100644
--- a/apisix/discovery/consul/schema.lua
+++ b/apisix/discovery/consul/schema.lua
@@ -24,6 +24,7 @@
                 type = "string",
             }
         },
+        token = {type = "string", default = ""},
         fetch_interval = {type = "integer", minimum = 1, default = 3},
         keepalive = {
             type = "boolean",
diff --git a/apisix/discovery/consul_kv/init.lua b/apisix/discovery/consul_kv/init.lua
index 2dad772..6d616e0 100644
--- a/apisix/discovery/consul_kv/init.lua
+++ b/apisix/discovery/consul_kv/init.lua
@@ -320,18 +320,14 @@
 
 local function format_consul_params(consul_conf)
     local consul_server_list = core.table.new(0, #consul_conf.servers)
-    local args
+    local args = {
+        token = consul_conf.token,
+        recurse = true
+    }
 
-    if consul_conf.keepalive == false then
-        args = {
-            recurse = true,
-        }
-    elseif consul_conf.keepalive then
-        args = {
-            recurse = true,
-            wait = consul_conf.timeout.wait, --blocked wait!=0; unblocked by wait=0
-            index = 0,
-        }
+    if consul_conf.keepalive then
+        args.wait = consul_conf.timeout.wait --blocked wait!=0; unblocked by wait=0
+        args.index = 0
     end
 
     for _, v in pairs(consul_conf.servers) do
diff --git a/apisix/discovery/consul_kv/schema.lua b/apisix/discovery/consul_kv/schema.lua
index a2ebb5d..4c02b2c 100644
--- a/apisix/discovery/consul_kv/schema.lua
+++ b/apisix/discovery/consul_kv/schema.lua
@@ -24,6 +24,7 @@
                 type = "string",
             }
         },
+        token = {type = "string", default = ""},
         fetch_interval = {type = "integer", minimum = 1, default = 3},
         keepalive = {
             type = "boolean",
diff --git a/ci/pod/docker-compose.first.yml b/ci/pod/docker-compose.first.yml
index 62ef7a3..aee79a8 100644
--- a/ci/pod/docker-compose.first.yml
+++ b/ci/pod/docker-compose.first.yml
@@ -46,6 +46,15 @@
     networks:
       consul_net:
 
+  consul_3:
+    image: hashicorp/consul:1.16.2
+    restart: unless-stopped
+    ports:
+      - "8502:8500"
+    command: [ "consul", "agent", "-server", "-bootstrap-expect=1", "-client", "0.0.0.0", "-log-level", "info", "-data-dir=/consul/data", "-enable-script-checks", "-ui", "-hcl", "acl = {\nenabled = true\ndefault_policy = \"deny\"\nenable_token_persistence = true\ntokens = {\nagent = \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\"\n}}" ]
+    networks:
+      consul_net:
+
   ## Nacos cluster
   nacos_auth:
     hostname: nacos1
diff --git a/docs/en/latest/discovery/consul.md b/docs/en/latest/discovery/consul.md
index b4eab61..85e6b9b 100644
--- a/docs/en/latest/discovery/consul.md
+++ b/docs/en/latest/discovery/consul.md
@@ -37,6 +37,7 @@
     servers:                      # make sure service name is unique in these consul servers
       - "http://127.0.0.1:8500"   # `http://127.0.0.1:8500` and `http://127.0.0.1:8600` are different clusters
       - "http://127.0.0.1:8600"   # `consul` service is default skip service
+    token: "..."                  # if your consul cluster has enabled acl access control, you need to specify the token
     skip_services:                # if you need to skip special services
       - "service_a"
     timeout:
diff --git a/docs/en/latest/discovery/consul_kv.md b/docs/en/latest/discovery/consul_kv.md
index bfb4344..e0a2602 100644
--- a/docs/en/latest/discovery/consul_kv.md
+++ b/docs/en/latest/discovery/consul_kv.md
@@ -40,6 +40,7 @@
     servers:
       - "http://127.0.0.1:8500"
       - "http://127.0.0.1:8600"
+    token: "..."                  # if your consul cluster has enabled acl access control, you need to specify the token
     prefix: "upstreams"
     skip_keys:                    # if you need to skip special keys
       - "upstreams/unused_api/"
diff --git a/t/control/discovery.t b/t/control/discovery.t
index c548b36..7bf81b1 100644
--- a/t/control/discovery.t
+++ b/t/control/discovery.t
@@ -77,7 +77,7 @@
 --- error_code: 200
 --- response_body
 {}
-{"fetch_interval":3,"keepalive":true,"prefix":"upstreams","servers":["http://127.0.0.1:8500","http://127.0.0.1:8600"],"timeout":{"connect":2000,"read":2000,"wait":60},"weight":1}
+{"fetch_interval":3,"keepalive":true,"prefix":"upstreams","servers":["http://127.0.0.1:8500","http://127.0.0.1:8600"],"timeout":{"connect":2000,"read":2000,"wait":60},"token":"","weight":1}
 --- error_log
 connect consul
 
diff --git a/t/discovery/consul.t b/t/discovery/consul.t
index 57a6ab5..739d9f8 100644
--- a/t/discovery/consul.t
+++ b/t/discovery/consul.t
@@ -112,6 +112,40 @@
         max_fails: 1
 _EOC_
 
+our $yaml_config_with_acl = <<_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:8502"
+    token: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
+    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();
 
@@ -657,3 +691,93 @@
     qr/server 1\n/,
 ]
 --- ignore_error_log
+
+
+
+=== TEST 14: bootstrap acl
+--- config
+location /v1/acl {
+    proxy_pass http://127.0.0.1:8502;
+}
+--- request eval
+"PUT /v1/acl/bootstrap\n" . "{\"BootstrapSecret\": \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\"}"
+--- error_code_like: ^(?:200|403)$
+
+
+
+=== TEST 15: test register and unregister nodes with acl
+--- yaml_config eval: $::yaml_config_with_acl
+--- 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:8502;
+    proxy_set_header X-Consul-Token "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a";
+}
+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
+--- pipelined_requests eval
+[
+    "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",
+
+    "PUT /v1/agent/service/deregister/service-a1",
+    "PUT /v1/agent/service/deregister/service-a2",
+]
+--- response_body_like eval
+[
+    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/,
+
+    qr//,
+    qr//
+]
+--- ignore_error_log
diff --git a/t/discovery/consul_kv.t b/t/discovery/consul_kv.t
index 9363f76..507410f 100644
--- a/t/discovery/consul_kv.t
+++ b/t/discovery/consul_kv.t
@@ -109,6 +109,37 @@
         max_fails: 1
 _EOC_
 
+our $yaml_config_with_acl = <<_EOC_;
+apisix:
+  node_listen: 1984
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+discovery:
+  consul_kv:
+    servers:
+      - "http://127.0.0.1:8502"
+    token: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
+    prefix: "upstreams"
+    skip_keys:
+      - "upstreams/unused_api/"
+    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();
 
@@ -576,3 +607,92 @@
 --- grep_error_log_out
 retry connecting consul after 1 seconds
 retry connecting consul after 4 seconds
+
+
+
+=== TEST 13: bootstrap acl
+--- config
+location /v1/acl {
+    proxy_pass http://127.0.0.1:8502;
+}
+--- request eval
+"PUT /v1/acl/bootstrap\n" . "{\"BootstrapSecret\": \"2b778dd9-f5f1-6f29-b4b4-9a5fa948757a\"}"
+--- error_code_like: ^(?:200|403)$
+
+
+
+=== TEST 14: test register and unregister nodes
+--- yaml_config eval: $::yaml_config_with_acl
+--- apisix_yaml
+routes:
+  -
+    uri: /*
+    upstream:
+      service_name: http://127.0.0.1:8502/v1/kv/upstreams/webpages/
+      discovery_type: consul_kv
+      type: roundrobin
+#END
+--- config
+location /v1/kv {
+    proxy_pass http://127.0.0.1:8502;
+    proxy_set_header X-Consul-Token "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a";
+}
+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
+[
+    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30511",
+    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30512",
+    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30513\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
+    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30514\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
+    "GET /sleep",
+
+    "GET /hello?random1",
+    "GET /hello?random2",
+    "GET /hello?random3",
+    "GET /hello?random4",
+
+    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30513",
+    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30514",
+    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
+    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
+    "GET /sleep?sec=5",
+
+    "GET /hello?random1",
+    "GET /hello?random2",
+    "GET /hello?random3",
+    "GET /hello?random4",
+
+]
+--- response_body_like eval
+[
+    qr/true/,
+    qr/true/,
+    qr/true/,
+    qr/true/,
+    qr/ok\n/,
+
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+    qr/server [3-4]\n/,
+
+    qr/true/,
+    qr/true/,
+    qr/true/,
+    qr/true/,
+    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