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/20 03:09:44 UTC

[apisix] branch master updated: feat(gRPC): support more features (#8485)

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 24d8e7cc7 feat(gRPC): support more features (#8485)
24d8e7cc7 is described below

commit 24d8e7cc74e57f1a0bd816f09dc890a5a4c8ad80
Author: 罗泽轩 <sp...@gmail.com>
AuthorDate: Tue Dec 20 11:09:31 2022 +0800

    feat(gRPC): support more features (#8485)
---
 apisix/cli/snippet.lua              |  12 +-
 apisix/core/etcd.lua                |  10 +-
 rockspec/apisix-master-0.rockspec   |   2 +-
 t/bin/gen_snippet.lua               |  16 +-
 t/cli/test_etcd_grpc.sh             |  70 +++++++++
 t/cli/test_etcd_grpc_healthcheck.sh | 153 +++++++++++++++++++
 t/core/etcd-grpc-auth-fail.t        | 106 +++++++++++++
 t/core/etcd-grpc-auth.t             | 108 +++++++++++++
 t/deployment/grpc/conf_server.t     | 293 ++++++++++++++++++++++++++++++++++++
 t/deployment/grpc/mtls.t            |   4 +-
 t/lib/etcd.proto                    |  32 ++++
 11 files changed, 793 insertions(+), 13 deletions(-)

diff --git a/apisix/cli/snippet.lua b/apisix/cli/snippet.lua
index 6225c213f..ca7ed3f7c 100644
--- a/apisix/cli/snippet.lua
+++ b/apisix/cli/snippet.lua
@@ -36,7 +36,11 @@ lua_ssl_trusted_certificate {* trusted_ca_cert *};
 
 server {
     {% if control_plane then %}
+    {% if directive_prefix == "grpc" then %}
+    listen {* control_plane.listen *} ssl http2;
+    {% else %}
     listen {* control_plane.listen *} ssl;
+    {% end %}
     ssl_certificate {* control_plane.cert *};
     ssl_certificate_key {* control_plane.cert_key *};
 
@@ -45,9 +49,13 @@ server {
     ssl_client_certificate {* control_plane.client_ca_cert *};
     {% end %}
 
+    {% else %}
+    {% if directive_prefix == "grpc" then %}
+    listen unix:{* home *}/conf/config_listen.sock http2;
     {% else %}
     listen unix:{* home *}/conf/config_listen.sock;
     {% end %}
+    {% end %}
 
     access_log off;
 
@@ -104,10 +112,6 @@ server {
 
 local function is_grpc_used(env, etcd)
     local is_grpc_available = env.use_apisix_base
-    if etcd.user then
-        -- TODO: support user/password
-        is_grpc_available = false
-    end
     return is_grpc_available and etcd.use_grpc
 end
 
diff --git a/apisix/core/etcd.lua b/apisix/core/etcd.lua
index 9e6786c86..d48eb9aa1 100644
--- a/apisix/core/etcd.lua
+++ b/apisix/core/etcd.lua
@@ -73,8 +73,7 @@ local function _new(etcd_conf)
     end
 
     if etcd_conf.use_grpc then
-        -- TODO: let lua-resty-etcd support more use cases
-        if etcd.user or ngx_get_phase() == "init" then
+        if ngx_get_phase() == "init" then
             etcd_conf.use_grpc = false
         else
             local ok = pcall(require, "resty.grpc")
@@ -125,9 +124,6 @@ local function new()
     end
 
     local etcd_conf = clone_tab(local_conf.etcd)
-    if etcd_conf.use_grpc then
-        return new_without_proxy()
-    end
 
     if local_conf.apisix.ssl and local_conf.apisix.ssl.ssl_trusted_certificate then
         etcd_conf.trusted_ca = local_conf.apisix.ssl.ssl_trusted_certificate
@@ -203,6 +199,10 @@ _M.new = new
 
 
 local function switch_proxy()
+    if ngx_get_phase() == "init" or ngx_get_phase() == "init_worker" then
+        return new_without_proxy()
+    end
+
     local etcd_cli, prefix, err = new()
     if not etcd_cli or err then
         return etcd_cli, prefix, err
diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec
index 9507002ed..24232c12d 100644
--- a/rockspec/apisix-master-0.rockspec
+++ b/rockspec/apisix-master-0.rockspec
@@ -34,7 +34,7 @@ dependencies = {
     "lua-resty-ctxdump = 0.1-0",
     "api7-lua-resty-dns-client = 7.0.1",
     "lua-resty-template = 2.0",
-    "lua-resty-etcd = 1.10.1",
+    "lua-resty-etcd = 1.10.2",
     "api7-lua-resty-http = 0.2.0",
     "lua-resty-balancer = 0.04",
     "lua-resty-ngxvar = 0.5.2",
diff --git a/t/bin/gen_snippet.lua b/t/bin/gen_snippet.lua
index 9ef21c328..b2ab349be 100755
--- a/t/bin/gen_snippet.lua
+++ b/t/bin/gen_snippet.lua
@@ -29,6 +29,7 @@ package.path  = pkg_path .. pkg_path_org
 local file = require("apisix.cli.file")
 local schema = require("apisix.cli.schema")
 local snippet = require("apisix.cli.snippet")
+local util = require("apisix.cli.util")
 local yaml_conf, err = file.read_yaml_conf("t/servroot")
 if not yaml_conf then
     error(err)
@@ -45,10 +46,23 @@ if not ok then
     error(err)
 end
 
+local or_info, err = util.execute_cmd("openresty -V 2>&1")
+if not or_info then
+    error("failed to exec cmd \'openresty -V 2>&1\', err: " .. err)
+end
+
+local use_apisix_base = true
+if not or_info:find("apisix-nginx-module", 1, true) then
+    use_apisix_base = false
+end
+
 local res, err
 if arg[1] == "conf_server" then
     res, err = snippet.generate_conf_server(
-        {apisix_home = "t/servroot/"},
+        {
+            apisix_home = "t/servroot/",
+            use_apisix_base = use_apisix_base,
+        },
         yaml_conf)
 end
 
diff --git a/t/cli/test_etcd_grpc.sh b/t/cli/test_etcd_grpc.sh
new file mode 100755
index 000000000..a10304905
--- /dev/null
+++ b/t/cli/test_etcd_grpc.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env 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.
+#
+
+# 'make init' operates scripts and related configuration files in the current directory
+# The 'apisix' command is a command in the /usr/local/apisix,
+# and the configuration file for the operation is in the /usr/local/apisix/conf
+
+. ./t/cli/common.sh
+
+exit_if_not_customed_nginx
+
+# check etcd while enable auth
+git checkout conf/config.yaml
+
+export ETCDCTL_API=3
+etcdctl version
+etcdctl --endpoints=127.0.0.1:2379 user add "root:apache-api6"
+etcdctl --endpoints=127.0.0.1:2379 role add root
+etcdctl --endpoints=127.0.0.1:2379 user grant-role root root
+etcdctl --endpoints=127.0.0.1:2379 user get root
+etcdctl --endpoints=127.0.0.1:2379 auth enable
+etcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 del /apisix --prefix
+
+echo '
+deployment:
+  role: traditional
+  role_traditional:
+    config_provider: etcd
+  etcd:
+    host:
+      - http://127.0.0.1:2379
+    prefix: /apisix
+    timeout: 30
+    use_grpc: true
+    user: root
+    password: apache-api6
+' > conf/config.yaml
+
+make run
+sleep 1
+
+code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1')
+make stop
+
+if [ ! $code -eq 200 ]; then
+    echo "failed: could not work with etcd"
+    exit 1
+fi
+
+echo "passed: work well with etcd auth enabled"
+
+etcdctl --endpoints=127.0.0.1:2379 --user=root:apache-api6 auth disable
+etcdctl --endpoints=127.0.0.1:2379 role delete root
+etcdctl --endpoints=127.0.0.1:2379 user delete root
diff --git a/t/cli/test_etcd_grpc_healthcheck.sh b/t/cli/test_etcd_grpc_healthcheck.sh
new file mode 100755
index 000000000..e57d98566
--- /dev/null
+++ b/t/cli/test_etcd_grpc_healthcheck.sh
@@ -0,0 +1,153 @@
+#!/usr/bin/env 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.
+#
+
+# 'make init' operates scripts and related configuration files in the current directory
+# The 'apisix' command is a command in the /usr/local/apisix,
+# and the configuration file for the operation is in the /usr/local/apisix/conf
+
+. ./t/cli/common.sh
+
+exit_if_not_customed_nginx
+
+custom_clean_up() {
+    clean_up
+
+    # stop etcd docker container
+    docker-compose -f ./t/cli/docker-compose-etcd-cluster.yaml down
+}
+
+trap custom_clean_up EXIT
+
+# create 3 node etcd cluster in docker
+ETCD_NAME_0=etcd0
+ETCD_NAME_1=etcd1
+ETCD_NAME_2=etcd2
+HEALTH_CHECK_RETRY_TIMEOUT=10
+
+if [ -f "logs/error.log" ]; then
+    rm logs/error.log
+fi
+touch logs/error.log
+
+echo '
+deployment:
+  role: traditional
+  role_traditional:
+    config_provider: etcd
+  etcd:
+    use_grpc: true
+    host:
+      - "http://0.0.0.0:23790"
+      - "http://0.0.0.0:23791"
+      - "http://0.0.0.0:23792"
+    health_check_timeout: '"$HEALTH_CHECK_RETRY_TIMEOUT"'
+    timeout: 2
+' > conf/config.yaml
+
+docker-compose -f ./t/cli/docker-compose-etcd-cluster.yaml up -d
+
+# case 1: Check apisix not got effected when one etcd node disconnected
+make init && make run
+
+docker stop ${ETCD_NAME_0}
+code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1')
+if [ ! $code -eq 200 ]; then
+    echo "failed: apisix got effect when one etcd node out of a cluster disconnected"
+    exit 1
+fi
+docker start ${ETCD_NAME_0}
+
+docker stop ${ETCD_NAME_1}
+code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1')
+if [ ! $code -eq 200 ]; then
+    echo "failed: apisix got effect when one etcd node out of a cluster disconnected"
+    exit 1
+fi
+docker start ${ETCD_NAME_1}
+
+make stop
+
+echo "passed: apisix not got effected when one etcd node disconnected"
+
+# case 2: Check when all etcd nodes disconnected, apisix trying to reconnect with backoff, and could successfully recover when reconnected
+make init && make run
+
+docker stop ${ETCD_NAME_0} && docker stop ${ETCD_NAME_1} && docker stop ${ETCD_NAME_2}
+
+sleep_till=$(date +%s -d "$DATE + $HEALTH_CHECK_RETRY_TIMEOUT second")
+
+code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1')
+if [ $code -eq 200 ]; then
+    echo "failed: apisix not got effect when all etcd nodes disconnected"
+    exit 1
+fi
+
+docker start ${ETCD_NAME_0} && docker start ${ETCD_NAME_1} && docker start ${ETCD_NAME_2}
+
+# case 3: sleep till etcd health check try to check again
+current_time=$(date +%s)
+sleep_seconds=$(( $sleep_till - $current_time + 3))
+if [ "$sleep_seconds" -gt 0 ]; then
+    sleep $sleep_seconds
+fi
+
+code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9180/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1')
+if [ ! $code -eq 200 ]; then
+    echo "failed: apisix could not recover when etcd node recover"
+    docker ps
+    cat logs/error.log
+    exit 1
+fi
+
+make stop
+
+echo "passed: when all etcd nodes disconnected, apisix trying to reconnect with backoff, and could successfully recover when reconnected"
+
+# case 4: stop one etcd node (result: start successful)
+docker stop ${ETCD_NAME_0}
+
+out=$(make init 2>&1)
+if echo "$out" | grep "23790" | grep "connection refused"; then
+    echo "passed: APISIX successfully to start, stop only one etcd node"
+else
+    echo "failed: stop only one etcd node APISIX should start normally"
+    exit 1
+fi
+
+# case 5: stop two etcd nodes (result: start failure)
+docker stop ${ETCD_NAME_1}
+
+out=$(make init 2>&1 || true)
+if echo "$out" | grep "23791" | grep "connection refused"; then
+    echo "passed: APISIX failed to start, etcd cluster must have two or more healthy nodes"
+else
+    echo "failed: two etcd nodes have been stopped, APISIX should fail to start"
+    exit 1
+fi
+
+# case 6: stop all etcd nodes (result: start failure)
+docker stop ${ETCD_NAME_2}
+
+out=$(make init 2>&1 || true)
+if echo "$out" | grep "23792" | grep "connection refused"; then
+    echo "passed: APISIX failed to start, all etcd nodes have stopped"
+else
+    echo "failed: all etcd nodes have stopped, APISIX should not be able to start"
+    exit 1
+fi
diff --git a/t/core/etcd-grpc-auth-fail.t b/t/core/etcd-grpc-auth-fail.t
new file mode 100644
index 000000000..b11f51ae2
--- /dev/null
+++ b/t/core/etcd-grpc-auth-fail.t
@@ -0,0 +1,106 @@
+#
+# 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.
+#
+BEGIN {
+    $ENV{"ETCD_ENABLE_AUTH"} = "false";
+    delete $ENV{"FLUSH_ETCD"};
+}
+
+use t::APISIX;
+
+my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
+my $version = eval { `$nginx_binary -V 2>&1` };
+
+if ($version !~ m/\/apisix-nginx-module/) {
+    plan(skip_all => "apisix-nginx-module not installed");
+} else {
+    plan('no_plan');
+}
+
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+# Authentication is enabled at etcd and credentials are set
+system('etcdctl --endpoints="http://127.0.0.1:2379" user add root:5tHkHhYkjr6cQY');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role add root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user grant-role root root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role list');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user user list');
+# Grant the user access to the specified directory
+system('etcdctl --endpoints="http://127.0.0.1:2379" user add apisix:abc123');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role add apisix');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user grant-role apisix apisix');
+system('etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission apisix --prefix=true readwrite /apisix/');
+system('etcdctl --endpoints="http://127.0.0.1:2379" auth enable');
+
+run_tests;
+
+# Authentication is disabled at etcd
+system('etcdctl --endpoints="http://127.0.0.1:2379" --user root:5tHkHhYkjr6cQY auth disable');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user delete root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role delete root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user delete apisix');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role delete apisix');
+__DATA__
+
+=== TEST 1: Set and Get a value pass
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local key = "/test_key"
+            local val = "test_value"
+            local res, err = core.etcd.set(key, val)
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- error_log eval
+qr /(insufficient credentials code: 401|etcdserver: user name is empty)/
+
+
+
+=== TEST 2: etcd grants permissions with a different prefix than the one used by apisix, etcd will forbidden
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local key = "/test_key"
+            local val = "test_value"
+            local res, err = core.etcd.set(key, val)
+            ngx.say(err)
+        }
+    }
+--- yaml_config
+deployment:
+  role: traditional
+  role_traditional:
+    config_provider: etcd
+  etcd:
+    host:
+      - "http://127.0.0.1:2379"
+    use_grpc: false
+    prefix: "/apisix"
+    user: apisix
+    password: abc123
+--- request
+GET /t
+--- error_log eval
+qr /etcd forbidden code: 403/
diff --git a/t/core/etcd-grpc-auth.t b/t/core/etcd-grpc-auth.t
new file mode 100644
index 000000000..12e2ce280
--- /dev/null
+++ b/t/core/etcd-grpc-auth.t
@@ -0,0 +1,108 @@
+#
+# 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.
+#
+BEGIN {
+    $ENV{"ETCD_ENABLE_AUTH"} = "true";
+    delete $ENV{"FLUSH_ETCD"};
+}
+
+use t::APISIX;
+
+my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx';
+my $version = eval { `$nginx_binary -V 2>&1` };
+
+if ($version !~ m/\/apisix-nginx-module/) {
+    plan(skip_all => "apisix-nginx-module not installed");
+} else {
+    plan('no_plan');
+}
+
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+log_level("info");
+
+# Authentication is enabled at etcd and credentials are set
+system('etcdctl --endpoints="http://127.0.0.1:2379" user add root:5tHkHhYkjr6cQY');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role add root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user grant-role root root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role list');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user user list');
+# Grant the user access to the specified directory
+system('etcdctl --endpoints="http://127.0.0.1:2379" user add apisix:abc123');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role add apisix');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user grant-role apisix apisix');
+system('etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission apisix --prefix=true readwrite /apisix');
+system('etcdctl --endpoints="http://127.0.0.1:2379" auth enable');
+
+run_tests;
+
+# Authentication is disabled at etcd
+system('etcdctl --endpoints="http://127.0.0.1:2379" --user root:5tHkHhYkjr6cQY auth disable');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user delete root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role delete root');
+system('etcdctl --endpoints="http://127.0.0.1:2379" user delete apisix');
+system('etcdctl --endpoints="http://127.0.0.1:2379" role delete apisix');
+
+
+__DATA__
+
+=== TEST 1: Set and Get a value pass with authentication
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local key = "/test_key"
+            local val = "test_value"
+            core.etcd.set(key, val)
+            local res, err = core.etcd.get(key)
+            ngx.say(res.body.node.value)
+            core.etcd.delete(val)
+        }
+    }
+--- request
+GET /t
+--- response_body
+test_value
+
+
+
+=== TEST 2: etcd grants permissions with the same prefix as apisix uses, etcd is normal
+--- config
+    location /t {
+        content_by_lua_block {
+            local core = require("apisix.core")
+            local key = "/test_key"
+            local val = "test_value"
+            local res, err = core.etcd.set(key, val)
+            ngx.say(err)
+        }
+    }
+--- yaml_config
+deployment:
+  role: traditional
+  role_traditional:
+    config_provider: etcd
+  etcd:
+    use_grpc: true
+    host:
+      - "http://127.0.0.1:2379"
+    prefix: "/apisix"
+    user: apisix
+    password: abc123
+--- request
+GET /t
diff --git a/t/deployment/grpc/conf_server.t b/t/deployment/grpc/conf_server.t
index a6ac47a59..5ea0bbe21 100644
--- a/t/deployment/grpc/conf_server.t
+++ b/t/deployment/grpc/conf_server.t
@@ -163,3 +163,296 @@ deployment:
             verify: false
 --- error_log
 Receive SNI: localhost
+
+
+
+=== TEST 4: check configured SNI
+--- http_config
+server {
+    listen 12345 http2 ssl;
+    ssl_certificate             cert/apisix.crt;
+    ssl_certificate_key         cert/apisix.key;
+
+    ssl_certificate_by_lua_block {
+        local ngx_ssl = require "ngx.ssl"
+        ngx.log(ngx.WARN, "Receive SNI: ", ngx_ssl.server_name())
+    }
+
+    location / {
+        grpc_pass grpc://127.0.0.1:2379;
+    }
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+        }
+    }
+--- response_body
+foo
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        prefix: "/apisix"
+        host:
+            - https://127.0.0.1:12379
+            - https://127.0.0.1:12345
+        timeout: 1
+        tls:
+            verify: false
+            sni: "x.com"
+--- error_log
+Receive SNI: x.com
+
+
+
+=== TEST 5: ipv6
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+        }
+    }
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        prefix: "/apisix"
+        host:
+            - http://[::1]:2379
+
+
+
+=== TEST 6: resolve domain, result changed
+--- extra_init_by_lua
+    local resolver = require("apisix.core.resolver")
+    local old_f = resolver.parse_domain
+    local counter = 0
+    resolver.parse_domain = function (domain)
+        if domain == "localhost" then
+            counter = counter + 1
+            if counter % 2 == 0 then
+                return "127.0.0.2"
+            else
+                return "127.0.0.3"
+            end
+        else
+            return old_f(domain)
+        end
+    end
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+        }
+    }
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        prefix: "/apisix"
+        host:
+            # use localhost so the connection is OK in the situation that the DNS
+            # resolve is not done in APISIX
+            - http://localhost:2379
+--- response_body
+foo
+--- error_log
+localhost is resolved to: 127.0.0.3
+localhost is resolved to: 127.0.0.2
+
+
+
+=== TEST 7: update balancer if the DNS result changed
+--- extra_init_by_lua
+    local etcd = require("apisix.core.etcd")
+    etcd.get_etcd_syncer = function ()
+        return etcd.new()
+    end
+
+    local resolver = require("apisix.core.resolver")
+    local old_f = resolver.parse_domain
+    package.loaded.counter = 0
+    resolver.parse_domain = function (domain)
+        if domain == "x.com" then
+            local counter = package.loaded.counter
+            package.loaded.counter = counter + 1
+            if counter % 2 == 0 then
+                return "127.0.0.2"
+            else
+                return "127.0.0.3"
+            end
+        else
+            return old_f(domain)
+        end
+    end
+
+    local picker = require("apisix.balancer.least_conn")
+    package.loaded.n_picker = 0
+    local old_f = picker.new
+    picker.new = function (nodes, upstream)
+        package.loaded.n_picker = package.loaded.n_picker + 1
+        return old_f(nodes, upstream)
+    end
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+            local counter = package.loaded.counter
+            local n_picker = package.loaded.n_picker
+            if counter == n_picker then
+                ngx.say("OK")
+            else
+                ngx.say(counter, " ", n_picker)
+            end
+        }
+    }
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        timeout: 1
+        prefix: "/apisix"
+        host:
+            - http://127.0.0.1:2379
+            - http://x.com:2379
+--- response_body
+foo
+OK
+--- error_log
+x.com is resolved to: 127.0.0.3
+x.com is resolved to: 127.0.0.2
+
+
+
+=== TEST 8: retry
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+        }
+    }
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        timeout: 1
+        prefix: "/apisix"
+        host:
+            - http://127.0.0.1:1979
+            - http://[::1]:1979
+            - http://localhost:2379
+--- error_log
+connect() failed
+--- response_body
+foo
+
+
+
+=== TEST 9: check Host header
+--- http_config
+server {
+    listen 12345 http2;
+    location / {
+        access_by_lua_block {
+            ngx.log(ngx.WARN, "Receive Host: ", ngx.var.http_host)
+        }
+        grpc_pass grpc://127.0.0.1:2379;
+    }
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+        }
+    }
+--- response_body
+foo
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        timeout: 1
+        prefix: "/apisix"
+        host:
+            - http://127.0.0.1:12345
+            - http://localhost:12345
+--- error_log
+Receive Host: localhost
+Receive Host: 127.0.0.1
+
+
+
+=== TEST 10: check Host header after retry
+--- http_config
+server {
+    listen 12345 http2;
+    location / {
+        access_by_lua_block {
+            ngx.log(ngx.WARN, "Receive Host: ", ngx.var.http_host)
+        }
+        grpc_pass grpc://127.0.0.1:2379;
+    }
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd = require("apisix.core.etcd")
+            assert(etcd.set("/apisix/test", "foo"))
+            local res = assert(etcd.get("/apisix/test"))
+            ngx.say(res.body.node.value)
+        }
+    }
+--- response_body
+foo
+--- yaml_config
+deployment:
+    role: traditional
+    role_traditional:
+        config_provider: etcd
+    etcd:
+        use_grpc: true
+        timeout: 1
+        prefix: "/apisix"
+        host:
+            - http://127.0.0.1:1979
+            - http://localhost:12345
+--- error_log
+Receive Host: localhost
diff --git a/t/deployment/grpc/mtls.t b/t/deployment/grpc/mtls.t
index abe0ea385..1aca2576d 100644
--- a/t/deployment/grpc/mtls.t
+++ b/t/deployment/grpc/mtls.t
@@ -40,9 +40,9 @@ __DATA__
 
 === TEST 1: mTLS for control plane
 --- exec
-curl --cert t/certs/mtls_client.crt --key t/certs/mtls_client.key -k https://localhost:12345/version
+grpcurl -import-path ./t/lib -proto etcd.proto -d '{}' -cert t/certs/mtls_client.crt -key t/certs/mtls_client.key -insecure localhost:12345 etcdserverpb.Maintenance.Status
 --- response_body eval
-qr/"etcdserver":/
+qr/"version":/
 --- yaml_config
 deployment:
     role: control_plane
diff --git a/t/lib/etcd.proto b/t/lib/etcd.proto
new file mode 100644
index 000000000..50dd0e5af
--- /dev/null
+++ b/t/lib/etcd.proto
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+package etcdserverpb;
+
+message StatusRequest {
+}
+
+message StatusResponse {
+  // version is the cluster protocol version used by the responding member.
+  string version = 2;
+}
+
+service Maintenance {
+  // Status gets the status of the member.
+  rpc Status(StatusRequest) returns (StatusResponse) {}
+}