From 7e68c8f8029f792efa33ef71f4ffc7ab9f06e5b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 19:43:33 +0800 Subject: [PATCH 001/260] chore(deps): bump apache/skywalking-eyes from 0.1.0 to 0.2.0 (#5231) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/license-checker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index 586c7a908fcc..6292bb716d6e 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -33,6 +33,6 @@ jobs: steps: - uses: actions/checkout@v2 - name: Check License Header - uses: apache/skywalking-eyes@v0.1.0 + uses: apache/skywalking-eyes@v0.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0e7ff54e6e7ef44bee85d9f38752486d9c104921 Mon Sep 17 00:00:00 2001 From: Yujia Qiao Date: Thu, 14 Oct 2021 19:44:16 +0800 Subject: [PATCH 002/260] fix: remove unexpected install target (#5235) --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index d1723577e345..dbdd6d7ae774 100644 --- a/Makefile +++ b/Makefile @@ -325,7 +325,6 @@ install: runtime $(INSTALL) -d $(INST_LUADIR)/apisix/utils $(INSTALL) apisix/utils/*.lua $(INST_LUADIR)/apisix/utils/ - $(INSTALL) README.md $(INST_CONFDIR)/README.md $(INSTALL) bin/apisix $(INST_BINDIR)/apisix $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/slslog From 8ac00e0610eded0f8e2738c5fb6afc0776434552 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:50:41 +0800 Subject: [PATCH 003/260] chore: docker-compose use fixed image tag to replace `latest` (#5242) --- ci/pod/docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index 64a781c81abf..c71ab63d6dc6 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -64,7 +64,7 @@ services: kafka_net: kafka-server1: - image: bitnami/kafka:latest + image: bitnami/kafka:2.8.1 env_file: - ci/pod/kafka/kafka-server/env/common.env environment: @@ -79,7 +79,7 @@ services: kafka_net: kafka-server2: - image: bitnami/kafka:latest + image: bitnami/kafka:2.8.1 env_file: - ci/pod/kafka/kafka-server/env/common.env environment: @@ -138,7 +138,7 @@ services: ## OpenLDAP openldap: - image: bitnami/openldap:latest + image: bitnami/openldap:2.5.8 environment: LDAP_ADMIN_USERNAME: amdin LDAP_ADMIN_PASSWORD: adminpassword From 298114f8d1471fa6070499f6d80ca0799c1adaa2 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Fri, 15 Oct 2021 09:45:55 +0800 Subject: [PATCH 004/260] test: make test etcd healthcheck stable (#5239) --- apisix/core/etcd.lua | 10 +++++++++- t/cli/test_etcd_healthcheck.sh | 5 ----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apisix/core/etcd.lua b/apisix/core/etcd.lua index 40a44ec85576..793ece52bd2d 100644 --- a/apisix/core/etcd.lua +++ b/apisix/core/etcd.lua @@ -17,10 +17,10 @@ local fetch_local_conf = require("apisix.core.config_local").local_conf local etcd = require("resty.etcd") local clone_tab = require("table.clone") +local health_check = require("resty.etcd.health_check") local ipairs = ipairs local string = string local tonumber = tonumber - local _M = {} @@ -56,6 +56,14 @@ local function new() end end + -- enable etcd health check retry for curr worker + if not health_check.conf then + health_check.init({ + max_fails = #etcd_conf.http_host, + retry = true, + }) + end + local etcd_cli etcd_cli, err = etcd.new(etcd_conf) if not etcd_cli then diff --git a/t/cli/test_etcd_healthcheck.sh b/t/cli/test_etcd_healthcheck.sh index bc7e3dc67964..34ca4d29a632 100755 --- a/t/cli/test_etcd_healthcheck.sh +++ b/t/cli/test_etcd_healthcheck.sh @@ -45,8 +45,6 @@ docker-compose -f ./t/cli/docker-compose-etcd-cluster.yaml up -d make init && make run docker stop ${ETCD_NAME_0} -# wait to etcd health check marks ETCD_NAME_0 as unhealthy -sleep 3 code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/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" @@ -55,9 +53,6 @@ fi docker start ${ETCD_NAME_0} docker stop ${ETCD_NAME_1} -# after 2 rounds of timeout, etcd health check marks ETCD_NAME_1 as unhealthy, -# and ETCD_NAME_1 is in fail_timeout state, it won't be selected to create a new etcd connection -sleep 5 code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/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" From d44d91a48f504586a65c938d35c4306419a26f98 Mon Sep 17 00:00:00 2001 From: Joey Date: Fri, 15 Oct 2021 19:19:55 +0800 Subject: [PATCH 005/260] chore: Add tips for awk failures in makefile (#5250) --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index dbdd6d7ae774..c5b548360d34 100644 --- a/Makefile +++ b/Makefile @@ -130,6 +130,8 @@ endif ### help : Show Makefile rules +### If there're awk failures, please make sure +### you are using awk or gawk .PHONY: help help: @$(call func_echo_success_status, "Makefile rules:") From 153e643674f13df98fb0929085ff61240aa73c66 Mon Sep 17 00:00:00 2001 From: Joey Date: Fri, 15 Oct 2021 23:12:41 +0800 Subject: [PATCH 006/260] docs: Add RPM repository guide for CentOS 7 (#5252) --- docs/en/latest/how-to-build.md | 24 +++++++++++++++++++++++- docs/zh/latest/how-to-build.md | 24 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 22b3ba9d6c7d..6fe600bf9480 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -29,7 +29,27 @@ Before installing Apache APISIX, please install dependencies according to the op ## Step 2: Install Apache APISIX -You can install Apache APISIX via RPM package, Docker, Helm Chart, and source release package. Please choose one from the following options. +You can install Apache APISIX via RPM Repository, RPM package, Docker, Helm Chart, and source release package. Please choose one from the following options. + +### Installation via RPM Repository(CentOS 7) + +This installation method is suitable for CentOS 7. For now, the Apache APISIX RPM repository for CentOS 7 is already supported. Please run the following commands to install the repository and Apache APISIX. + +```shell +sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo +# View apisix package information, only 2.10.0 is included for now +sudo yum info -y apisix +sudo yum --showduplicates list apisix + +# Will install apisix-2.10.0 +sudo yum install apisix +``` + +If the official OpenResty repository is not installed yet, the following command will help you automatically install both the repositories of OpenResty and Apache APISIX. + +```shell +sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +``` ### Installation via RPM Package(CentOS 7) @@ -39,6 +59,8 @@ This installation method is suitable for CentOS 7, please run the following comm sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.0/apisix-2.10.0-0.el7.x86_64.rpm ``` +> You can also install the RPM package via running `sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.0-0.el7.x86_64.rpm`. + ### Installation via Docker Please refer to: [Installing Apache APISIX with Docker](https://hub.docker.com/r/apache/apisix). diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index c4195911e1a0..e4bdbe1d4d7e 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -29,7 +29,27 @@ Apache APISIX 的运行环境需要依赖 NGINX 和 etcd,所以在安装 Apach ## 步骤2:安装 Apache APISIX -你可以通过 RPM 包、Docker、Helm Chart、源码包等多种方式来安装 Apache APISIX。请在以下选项中选择其中一种执行。 +你可以通过 RPM 仓库、RPM 包、Docker、Helm Chart、源码包等多种方式来安装 Apache APISIX。请在以下选项中选择其中一种执行。 + +### 通过 RPM 仓库安装(CentOS 7) + +这种安装方式适用于 CentOS 7 操作系统。Apache APISIX 已经支持适用于 CentOS 7 的 RPM 仓库。请运行以下命令安装 RPM 仓库和 Apache APISIX。 + +```shell +sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo +# View apisix package information, only 2.10.0 is included for now +sudo yum info -y apisix +sudo yum --showduplicates list apisix + +# Will install apisix-2.10.0 +sudo yum install apisix +``` + +如果尚未安装 OpenResty 的官方 RPM 仓库,以下命令可以帮助您自动安装 OpenResty 和 Apache APISIX 的 RPM 仓库。 + +```shell +sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +``` ### 通过 RPM 包安装(CentOS 7) @@ -39,6 +59,8 @@ Apache APISIX 的运行环境需要依赖 NGINX 和 etcd,所以在安装 Apach sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.0/apisix-2.10.0-0.el7.x86_64.rpm ``` +> 您也可以运行 `sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.0-0.el7.x86_64.rpm` 命令安装。 + ### 通过 Docker 安装 详情请参考:[使用 Docker 安装 Apache APISIX](https://hub.docker.com/r/apache/apisix)。 From 053c3fa57b8efd2be52ad63829f10bdcc332744e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 17 Oct 2021 19:17:11 +0800 Subject: [PATCH 007/260] chore(client-control): update outdated error msg (#5245) --- apisix/plugins/client-control.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugins/client-control.lua b/apisix/plugins/client-control.lua index b6dff071ef70..89e7e8b36642 100644 --- a/apisix/plugins/client-control.lua +++ b/apisix/plugins/client-control.lua @@ -49,7 +49,7 @@ end function _M.rewrite(conf, ctx) if not ok then - core.log.error("need to build APISIX-OpenResty to support client restriction") + core.log.error("need to build APISIX-OpenResty to support client control") return 501 end From c36ebee7caa2d01de3b1f80996278b9f4cb2b9b5 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Sun, 17 Oct 2021 19:26:01 +0800 Subject: [PATCH 008/260] fix(zipkin): the zipkin ctx isn't reused (#5256) --- apisix/plugins/zipkin.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apisix/plugins/zipkin.lua b/apisix/plugins/zipkin.lua index cb261c664122..e292b62ae64f 100644 --- a/apisix/plugins/zipkin.lua +++ b/apisix/plugins/zipkin.lua @@ -279,9 +279,9 @@ function _M.log(conf, ctx) opentracing.request_span:finish(log_end_time) - if ctx.zipkin_ctx then - core.tablepool.release("zipkin_ctx", ctx.zipkin_ctx) - ctx.zipkin_ctx = nil + if ctx.zipkin then + core.tablepool.release("zipkin_ctx", ctx.zipkin) + ctx.zipkin = nil end end From 4395d708e9e61944210e45f1e6a079f22d644170 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 18 Oct 2021 09:04:19 +0800 Subject: [PATCH 009/260] feat(ext-plugin): avoid sending conf request more times (#5183) --- apisix/cli/ngx_tpl.lua | 3 + apisix/plugins/ext-plugin/init.lua | 51 +++++++++- conf/config-default.yaml | 1 + t/APISIX.pm | 1 + t/plugin/ext-plugin/conf_token.t | 145 +++++++++++++++++++++++++++++ t/plugin/ext-plugin/sanity.t | 2 + 6 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 t/plugin/ext-plugin/conf_token.t diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 07ad5ea35454..6b78764f875f 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -191,6 +191,9 @@ http { # for authz-keycloak lua_shared_dict access-tokens {* http.lua_shared_dict["access-tokens"] *}; # cache for service account access tokens + # for ext-plugin + lua_shared_dict ext-plugin {* http.lua_shared_dict["ext-plugin"] *}; # cache for ext-plugin + # for custom shared dict {% if http.custom_lua_shared_dict then %} {% for cache_key, cache_size in pairs(http.custom_lua_shared_dict) do %} diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index 6cb593c8c1d1..afc25ff65908 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -60,6 +60,7 @@ local ipairs = ipairs local pairs = pairs local tostring = tostring local type = type +local dict = ngx.shared["ext-plugin"] local events_list @@ -292,6 +293,44 @@ local function handle_extra_info(ctx, input) end +local function fetch_token(key) + if dict then + return dict:get(key) + else + core.log.error('shm "ext-plugin" not found') + return nil + end +end + + +local function store_token(key, token) + if dict then + local exp = helper.get_conf_token_cache_time() + -- early expiry, lrucache in critical state sends prepare_conf_req as original behaviour + exp = exp * 0.9 + local success, err, forcible = dict:set(key, token, exp) + if not success then + core.log.error("ext-plugin:failed to set conf token, err: ", err) + end + if forcible then + core.log.warn("ext-plugin:set valid items forcibly overwritten") + end + else + core.log.error('shm "ext-plugin" not found') + end +end + + +local function flush_token() + if dict then + core.log.warn("flush conf token in shared dict") + dict:flush_all() + else + core.log.error('shm "ext-plugin" not found') + end +end + + local rpc_call local rpc_handlers = { nil, @@ -300,6 +339,12 @@ local rpc_handlers = { local key = builder:CreateString(unique_key) + local token = fetch_token(key) + if token then + core.log.info("fetch token from shared dict, token: ", token) + return token + end + local conf_vec if conf.conf then local len = #conf.conf @@ -344,9 +389,10 @@ local rpc_handlers = { local buf = flatbuffers.binaryArray.New(resp) local pcr = prepare_conf_resp.GetRootAsResp(buf, 0) - local token = pcr:ConfToken() + token = pcr:ConfToken() core.log.notice("get conf token: ", token, " conf: ", core.json.delay_encode(conf.conf)) + store_token(key, token) return token end, function (conf, ctx, sock, entry) @@ -470,7 +516,6 @@ local rpc_handlers = { local buf = flatbuffers.binaryArray.New(resp) local call_resp = http_req_call_resp.GetRootAsResp(buf, 0) local action_type = call_resp:ActionType() - if action_type == http_req_call_action.Stop then local action = call_resp:Action() local stop = http_req_call_stop.New() @@ -588,6 +633,8 @@ end local function create_lrucache() + flush_token() + if lrucache then core.log.warn("flush conf token lrucache") end diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 9436821ce60d..0df2fa92586f 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -253,6 +253,7 @@ nginx_config: # config for render the template to generate n jwks: 1m introspection: 10m access-tokens: 1m + ext-plugin: 1m etcd: host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster. diff --git a/t/APISIX.pm b/t/APISIX.pm index 5b960cc87f8b..83e28c810a7e 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -445,6 +445,7 @@ _EOC_ lua_shared_dict plugin-api-breaker 10m; lua_capture_error_log 1m; # plugin error-log-logger lua_shared_dict etcd-cluster-health-check 10m; # etcd health check + lua_shared_dict ext-plugin 1m; proxy_ssl_name \$upstream_host; proxy_ssl_server_name on; diff --git a/t/plugin/ext-plugin/conf_token.t b/t/plugin/ext-plugin/conf_token.t new file mode 100644 index 000000000000..a33c674921b3 --- /dev/null +++ b/t/plugin/ext-plugin/conf_token.t @@ -0,0 +1,145 @@ +# +# 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'; + +workers(8); +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); +log_level("info"); +worker_connections(10240); + +$ENV{"PATH"} = $ENV{PATH} . ":" . $ENV{TEST_NGINX_HTML_DIR}; + +add_block_preprocessor(sub { + my ($block) = @_; + + $block->set_value("stream_conf_enable", 1); + + if (!defined $block->extra_stream_config) { + my $stream_config = <<_EOC_; + server { + listen unix:\$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + ext.go({}) + } + } + +_EOC_ + $block->set_value("extra_stream_config", $stream_config); + } + + my $unix_socket_path = $ENV{"TEST_NGINX_HTML_DIR"} . "/nginx.sock"; + my $orig_extra_yaml_config = $block->extra_yaml_config // ""; + my $cmd = $block->ext_plugin_cmd // "['sleep', '5s']"; + my $extra_yaml_config = <<_EOC_; +ext-plugin: + path_for_test: $unix_socket_path + cmd: $cmd +_EOC_ + $extra_yaml_config = $extra_yaml_config . $orig_extra_yaml_config; + + $block->set_value("extra_yaml_config", $extra_yaml_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + + local code, message, res = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "ext-plugin-pre-req": {"a":"b"} + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(message) + return + end + + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 2: share conf token in different workers +--- ext_plugin_cmd +["t/plugin/ext-plugin/runner.sh", "3600"] +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + + local t = {} + for i = 1, 180 do + local th = assert(ngx.thread.spawn(function(i) + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.log(ngx.ERR, err) + return + end + end, i)) + table.insert(t, th) + end + for i, th in ipairs(t) do + ngx.thread.wait(th) + end + ngx.say("done") + } + } +--- response_body +done +--- grep_error_log eval +qr/fetch token from shared dict, token: 233/ +--- grep_error_log_out eval +qr/(fetch token from shared dict, token: 233){1,}/ +--- no_error_log +[error] diff --git a/t/plugin/ext-plugin/sanity.t b/t/plugin/ext-plugin/sanity.t index 75b01f98c26e..2a5e965ffc3a 100644 --- a/t/plugin/ext-plugin/sanity.t +++ b/t/plugin/ext-plugin/sanity.t @@ -270,6 +270,7 @@ sending rpc type: 1 data length: receiving rpc type: 1 data length: --- error_log flush conf token lrucache +flush conf token in shared dict --- no_error_log [error] @@ -382,6 +383,7 @@ hello world } --- error_log refresh cache and try again +flush conf token in shared dict --- no_error_log [error] From c46213a6e2e579f59473a0145940bbe05a0aebfb Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 18 Oct 2021 09:17:02 +0800 Subject: [PATCH 010/260] fix: route's timeout should not be overwrittern by service (#5219) --- apisix/plugin.lua | 4 +++ t/node/timeout-upstream.t | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index cee49bddf801..1e060cd6e11f 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -412,6 +412,10 @@ local function merge_service_route(service_conf, route_conf) new_conf.value.script = route_conf.value.script end + if route_conf.value.timeout then + new_conf.value.timeout = route_conf.value.timeout + end + if route_conf.value.name then new_conf.value.name = route_conf.value.name else diff --git a/t/node/timeout-upstream.t b/t/node/timeout-upstream.t index c86055917cdd..a7dfb8b11271 100644 --- a/t/node/timeout-upstream.t +++ b/t/node/timeout-upstream.t @@ -128,3 +128,70 @@ GET /mysleep?seconds=1 qr/504 Gateway Time-out/ --- error_log timed out) while reading response header from upstream + + + +=== TEST 5: set route inherit hosts from service +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local scode, sbody = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "desc":"test-service", + "hosts": ["foo.com"] + }]] + ) + + if scode >= 300 then + ngx.status = scode + end + ngx.say(sbody) + + local rcode, rbody = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "service_id": "1", + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin", + "timeout": { + "connect": 0.5, + "send": 0.5, + "read": 0.5 + } + }, + "uri": "/mysleep" + }]] + ) + + if rcode >= 300 then + ngx.status = rcode + end + ngx.say(rbody) + } + } +--- request +GET /t +--- response_body +passed +passed +--- no_error_log +[error] + + + +=== TEST 6: hit service route (timeout) +--- request +GET /mysleep?seconds=1 +--- more_headers +Host: foo.com +--- error_code: 504 +--- response_body eval +qr/504 Gateway Time-out/ +--- error_log +timed out) while reading response header from upstream From 9abd12952d4485808bf3a9e515552bffa710712f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Oct 2021 17:46:02 +0800 Subject: [PATCH 011/260] chore(deps): bump actions/checkout from 2.3.4 to 2.3.5 (#5262) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/centos7-ci.yml | 2 +- .github/workflows/chaos.yml | 2 +- .github/workflows/cli.yml | 2 +- .github/workflows/code-lint.yml | 2 +- .github/workflows/doc-lint.yml | 2 +- .github/workflows/fuzzing-ci.yaml | 2 +- .github/workflows/license-checker.yml | 2 +- .github/workflows/lint.yml | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af26440b0cce..c8fc220037fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 with: submodules: recursive diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 1c8c97a78ded..cffea25f8908 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 with: submodules: recursive diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 99f639d0c363..e5c4bf7e68de 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -15,7 +15,7 @@ jobs: chaos-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v2.3.5 with: submodules: recursive diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 571d923be0a7..7bd22edba3c8 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 with: submodules: recursive diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index be995b460c75..fa8527c19a32 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -10,7 +10,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v2.3.5 - name: Install run: | . ./ci/common.sh diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 891b0296a387..4e4428afa694 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -11,7 +11,7 @@ jobs: name: 🍇 Markdown runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v2.3.5 - name: 🚀 Use Node.js uses: actions/setup-node@v2.4.1 with: diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index 32e9c331e4dc..932ef18d5143 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -23,7 +23,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 with: submodules: recursive diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index 6292bb716d6e..00d1363d9cac 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.5 - name: Check License Header uses: apache/skywalking-eyes@v0.2.0 env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1c8436f2e236..337bd1e05c98 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: name: 🌌 Trailing whitespace runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v2.3.5 - name: 🧹 Check for trailing whitespace run: "! git grep -EIn $'[ \t]+$'" misc: @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code. - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Install run: | wget -O - -q https://git.io/misspell | sh -s -- -b . From e71052783c15b4ccccedd25e78a53de5c5787370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Mon, 18 Oct 2021 17:47:31 +0800 Subject: [PATCH 012/260] fix: shm health check is not enabled after upgrading etcd client to 1.6.0 (#5257) --- apisix/core/config_etcd.lua | 2 +- t/core/config_etcd.t | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua index f3ac321760f2..fa2b09e90dc6 100644 --- a/apisix/core/config_etcd.lua +++ b/apisix/core/config_etcd.lua @@ -518,7 +518,7 @@ local function _automatic_fetch(premature, self) return end - if not health_check.conf then + if not (health_check.conf and health_check.conf.shm_name) then local _, err = health_check.init({ shm_name = health_check_shm_name, fail_timeout = self.health_check_timeout, diff --git a/t/core/config_etcd.t b/t/core/config_etcd.t index 1377afa6e1dd..f5a377dea7a9 100644 --- a/t/core/config_etcd.t +++ b/t/core/config_etcd.t @@ -316,3 +316,32 @@ passed passed --- no_error_log [error] + + + +=== TEST 9: Test ETCD health check mode switch during APISIX startup +apisix: + node_listen: 1984 + admin_key: null +etcd: + host: + - "http://127.0.0.1:2379" + tls: + verify: false + prefix: "/apisix" +--- config + location /t { + content_by_lua_block { + ngx.say("passed") + } + } +--- request +GET /t +--- response_body +passed +--- grep_error_log eval +qr/healthy check use \S+ \w+/ +--- grep_error_log_out +healthy check use round robin +healthy check use ngx.shared dict +healthy check use ngx.shared dict From 6798a7ce12c2994a120cb31b5f4e81a9e676d648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 18 Oct 2021 20:43:50 +0800 Subject: [PATCH 013/260] feat: use lock to ensure fetching token from shdict always (#5263) --- apisix/plugins/ext-plugin/init.lua | 46 +++++++++++++++++++++++------- t/lib/ext-plugin.lua | 2 +- t/plugin/ext-plugin/conf_token.t | 8 ++---- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index afc25ff65908..72d89fcb6490 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -40,6 +40,7 @@ if is_http then ngx_pipe = require("ngx.pipe") events = require("resty.worker.events") end +local resty_lock = require("resty.lock") local resty_signal = require "resty.signal" local bit = require("bit") local band = bit.band @@ -60,7 +61,6 @@ local ipairs = ipairs local pairs = pairs local tostring = tostring local type = type -local dict = ngx.shared["ext-plugin"] local events_list @@ -68,6 +68,8 @@ local lrucache = core.lrucache.new({ type = "plugin", ttl = helper.get_conf_token_cache_time(), }) +local shdict_name = "ext-plugin" +local shdict = ngx.shared[shdict_name] local schema = { type = "object", @@ -294,8 +296,8 @@ end local function fetch_token(key) - if dict then - return dict:get(key) + if shdict then + return shdict:get(key) else core.log.error('shm "ext-plugin" not found') return nil @@ -304,11 +306,11 @@ end local function store_token(key, token) - if dict then + if shdict then local exp = helper.get_conf_token_cache_time() -- early expiry, lrucache in critical state sends prepare_conf_req as original behaviour exp = exp * 0.9 - local success, err, forcible = dict:set(key, token, exp) + local success, err, forcible = shdict:set(key, token, exp) if not success then core.log.error("ext-plugin:failed to set conf token, err: ", err) end @@ -322,9 +324,9 @@ end local function flush_token() - if dict then + if shdict then core.log.warn("flush conf token in shared dict") - dict:flush_all() + shdict:flush_all() else core.log.error('shm "ext-plugin" not found') end @@ -335,16 +337,32 @@ local rpc_call local rpc_handlers = { nil, function (conf, ctx, sock, unique_key) - builder:Clear() + local token = fetch_token(unique_key) + if token then + core.log.info("fetch token from shared dict, token: ", token) + return token + end - local key = builder:CreateString(unique_key) + local lock, err = resty_lock:new(shdict_name) + if not lock then + return nil, "failed to create lock: " .. err + end - local token = fetch_token(key) + local elapsed, err = lock:lock("prepare_conf") + if not elapsed then + return nil, "failed to acquire the lock: " .. err + end + + local token = fetch_token(unique_key) if token then + lock:unlock() core.log.info("fetch token from shared dict, token: ", token) return token end + builder:Clear() + + local key = builder:CreateString(unique_key) local conf_vec if conf.conf then local len = #conf.conf @@ -375,15 +393,18 @@ local rpc_handlers = { local ok, err = send(sock, constants.RPC_PREPARE_CONF, builder:Output()) if not ok then + lock:unlock() return nil, "failed to send RPC_PREPARE_CONF: " .. err end local ty, resp = receive(sock) if ty == nil then + lock:unlock() return nil, "failed to receive RPC_PREPARE_CONF: " .. resp end if ty ~= constants.RPC_PREPARE_CONF then + lock:unlock() return nil, "failed to receive RPC_PREPARE_CONF: unexpected type " .. ty end @@ -392,7 +413,10 @@ local rpc_handlers = { token = pcr:ConfToken() core.log.notice("get conf token: ", token, " conf: ", core.json.delay_encode(conf.conf)) - store_token(key, token) + store_token(unique_key, token) + + lock:unlock() + return token end, function (conf, ctx, sock, entry) diff --git a/t/lib/ext-plugin.lua b/t/lib/ext-plugin.lua index c005c80cd49c..f38951a7f746 100644 --- a/t/lib/ext-plugin.lua +++ b/t/lib/ext-plugin.lua @@ -54,7 +54,7 @@ end function _M.go(case) - local sock = ngx.req.socket() + local sock = ngx.req.socket(true) local ty, data = ext.receive(sock) if not ty then ngx.log(ngx.ERR, data) diff --git a/t/plugin/ext-plugin/conf_token.t b/t/plugin/ext-plugin/conf_token.t index a33c674921b3..58a8b1372528 100644 --- a/t/plugin/ext-plugin/conf_token.t +++ b/t/plugin/ext-plugin/conf_token.t @@ -16,13 +16,13 @@ # use t::APISIX 'no_plan'; -workers(8); +workers(3); repeat_each(1); no_long_string(); no_root_location(); no_shuffle(); log_level("info"); -worker_connections(10240); +worker_connections(1024); $ENV{"PATH"} = $ENV{PATH} . ":" . $ENV{TEST_NGINX_HTML_DIR}; @@ -109,8 +109,6 @@ passed === TEST 2: share conf token in different workers ---- ext_plugin_cmd -["t/plugin/ext-plugin/runner.sh", "3600"] --- config location /t { content_by_lua_block { @@ -118,7 +116,7 @@ passed local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" local t = {} - for i = 1, 180 do + for i = 1, 16 do local th = assert(ngx.thread.spawn(function(i) local httpc = http.new() local res, err = httpc:request_uri(uri) From ae08d23d9730141d88caca49ceb97073946e56fd Mon Sep 17 00:00:00 2001 From: Yujia Qiao Date: Mon, 18 Oct 2021 20:59:50 +0800 Subject: [PATCH 014/260] docs: fix the indent of README.md (#5230) --- README.md | 6 +++--- docs/zh/latest/README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d7e90486afa1..248c868fd5cb 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,11 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against 1. Installation -APISIX Installed and tested in the following systems: + APISIX Installed and tested in the following systems: -CentOS 7, Ubuntu 16.04, Ubuntu 18.04, Debian 9, Debian 10, macOS, **ARM64** Ubuntu 18.04 + CentOS 7, Ubuntu 16.04, Ubuntu 18.04, Debian 9, Debian 10, macOS, **ARM64** Ubuntu 18.04 -Please refer to [install documentation](docs/en/latest/how-to-build.md). + Please refer to [install documentation](docs/en/latest/how-to-build.md). 2. Getting started diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index 85932a4a9498..d0eed94dd478 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -149,11 +149,11 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 1. 安装 -APISIX 在以下操作系统中可顺利安装并做过测试: + APISIX 在以下操作系统中可顺利安装并做过测试: -CentOS 7, Ubuntu 16.04, Ubuntu 18.04, Debian 9, Debian 10, macOS, **ARM64** Ubuntu 18.04 + CentOS 7, Ubuntu 16.04, Ubuntu 18.04, Debian 9, Debian 10, macOS, **ARM64** Ubuntu 18.04 -请参考[安装文档](./how-to-build.md)。 + 请参考[安装文档](./how-to-build.md)。 2. 入门指南 From 021828018ddcea6cbe6f3b4969051f528bfc9435 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Tue, 19 Oct 2021 10:24:52 +0530 Subject: [PATCH 015/260] docs: update invalid links (#5266) --- conf/config-default.yaml | 4 ++-- docs/en/latest/how-to-build.md | 2 +- docs/en/latest/mtls.md | 2 +- docs/en/latest/plugin-develop.md | 4 ++-- docs/en/latest/router-radixtree.md | 2 +- docs/zh/latest/how-to-build.md | 2 +- docs/zh/latest/mtls.md | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 0df2fa92586f..1cdb76fe6111 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -266,8 +266,8 @@ etcd: #user: root # root username for etcd #password: 5tHkHhYkjr6cQY # root password for etcd tls: - # To enable etcd client certificate you need to build APISIX-Openresty, see - # http://apisix.apache.org/docs/apisix/how-to-build#6-build-openresty-for-apisix + # To enable etcd client certificate you need to build APISIX-OpenResty, see + # https://apisix.apache.org/docs/apisix/how-to-build/#step-6-build-openresty-for-apache-apisix #cert: /path/to/cert # path of certificate used by the etcd client #key: /path/to/key # path of key used by the etcd client diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 6fe600bf9480..799c47bedd95 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -204,7 +204,7 @@ apisix help The solution to the `Error unknown directive "lua_package_path" in /API_ASPIX/apisix/t/servroot/conf/nginx.conf` error is as shown below. -Ensure that Openresty is set to the default NGINX, and export the path as follows: +Ensure that OpenResty is set to the default NGINX, and export the path as follows: * `export PATH=/usr/local/openresty/nginx/sbin:$PATH` * Linux default installation path: diff --git a/docs/en/latest/mtls.md b/docs/en/latest/mtls.md index cf1c2a103ca9..cd364ecee955 100644 --- a/docs/en/latest/mtls.md +++ b/docs/en/latest/mtls.md @@ -66,7 +66,7 @@ curl --cacert /data/certs/mtls_ca.crt --key /data/certs/mtls_client.key --cert / ### How to configure -You need to [build APISIX-Openresty](./how-to-build.md#step-6-build-openresty-for-apache-apisix) and configure `etcd.tls` section if you want APISIX to work on an etcd cluster with mTLS enabled. +You need to [build APISIX-OpenResty](./how-to-build.md#step-6-build-openresty-for-apache-apisix) and configure `etcd.tls` section if you want APISIX to work on an etcd cluster with mTLS enabled. ```yaml etcd: diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md index d0797c308dc4..b6c60854b3ab 100644 --- a/docs/en/latest/plugin-develop.md +++ b/docs/en/latest/plugin-develop.md @@ -26,7 +26,7 @@ title: Plugin Develop - [table of contents](#table-of-contents) - [where to put your plugins](#where-to-put-your-plugins) - [check dependencies](#check-dependencies) -- [name and config](#name-and-config) +- [name, priority and the others](#name-priority-and-the-others) - [schema and check](#schema-and-check) - [choose phase to run](#choose-phase-to-run) - [implement the logic](#implement-the-logic) @@ -311,7 +311,7 @@ end ## choose phase to run -Determine which phase to run, generally access or rewrite. If you don't know the [Openresty life cycle](https://openresty-reference.readthedocs.io/en/latest/Directives/), it's +Determine which phase to run, generally access or rewrite. If you don't know the [OpenResty lifecycle](https://github.com/openresty/lua-nginx-module/blob/master/README.markdown#directives), it's recommended to know it in advance. For example key-auth is an authentication plugin, thus the authentication should be completed before forwarding the request to any upstream service. Therefore, the plugin must be executed in the rewrite phases. In APISIX, only the authentication logic can be run in the rewrite phase. Other logic needs to run before proxy should be in access phase. diff --git a/docs/en/latest/router-radixtree.md b/docs/en/latest/router-radixtree.md index d94d8a7f3a21..a65e90cac180 100644 --- a/docs/en/latest/router-radixtree.md +++ b/docs/en/latest/router-radixtree.md @@ -29,7 +29,7 @@ APISIX using libradixtree as route dispatching library. ### How to use libradixtree in APISIX? -This is Lua-Openresty implementation library base on FFI for [rax](https://github.com/antirez/rax). +This is Lua-OpenResty implementation library base on FFI for [rax](https://github.com/antirez/rax). Let's take a look at a few examples and have an intuitive understanding. diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index e4bdbe1d4d7e..cebac9ecbb29 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -202,7 +202,7 @@ apisix help 出现`Error unknown directive "lua_package_path" in /API_ASPIX/apisix/t/servroot/conf/nginx.conf` 报错的解决方法如下: -确保将 Openresty 设置为默认的 NGINX,并按如下所示导出路径: +确保将 OpenResty 设置为默认的 NGINX,并按如下所示导出路径: * `export PATH=/usr/local/openresty/nginx/sbin:$PATH` * Linux 默认安装路径: diff --git a/docs/zh/latest/mtls.md b/docs/zh/latest/mtls.md index 739ea130059a..3a4ed70d271f 100644 --- a/docs/zh/latest/mtls.md +++ b/docs/zh/latest/mtls.md @@ -66,7 +66,7 @@ curl --cacert /data/certs/mtls_ca.crt --key /data/certs/mtls_client.key --cert / ### 如何配置 -你需要构建 [APISIX-Openresty](./how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty),并且需要在配置文件中设定 `etcd.tls` 来使 ETCD 的双向认证功能正常工作。 +你需要构建 [APISIX-OpenResty](./how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty),并且需要在配置文件中设定 `etcd.tls` 来使 ETCD 的双向认证功能正常工作。 ```yaml etcd: From 50fed630823bb3c562f411d7cb5f5d38218348fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=AE=AD=E7=81=BC?= Date: Tue, 19 Oct 2021 12:57:41 +0800 Subject: [PATCH 016/260] feat(control): add dump upstream api (#5259) --- apisix/control/v1.lua | 54 ++++++++++++- docs/en/latest/control-api.md | 80 +++++++++++++++++++ t/control/upstreams.t | 144 ++++++++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 t/control/upstreams.t diff --git a/apisix/control/v1.lua b/apisix/control/v1.lua index 9b5dcac75ec2..64d778a2cad4 100644 --- a/apisix/control/v1.lua +++ b/apisix/control/v1.lua @@ -178,7 +178,7 @@ end function _M.dump_all_routes_info() local routes = get_routes() - local infos, _ = iter_add_get_routes_info(routes, nil) + local infos = iter_add_get_routes_info(routes, nil) return 200, infos end @@ -193,7 +193,45 @@ function _M.dump_route_info() return 200, route end +local function iter_add_get_upstream_info(values, upstream_id) + if not values then + return nil + end + local infos = {} + for _, upstream in core.config_util.iterate_values(values) do + local new_upstream = core.table.deepcopy(upstream) + core.table.insert(infos, new_upstream) + if new_upstream.value and new_upstream.value.parent then + new_upstream.value.parent = nil + end + -- check the upstream id + if upstream_id and upstream.value.id == upstream_id then + return new_upstream + end + end + if not upstream_id then + return infos + end + return nil +end + +function _M.dump_all_upstreams_info() + local upstreams = get_upstreams() + local infos = iter_add_get_upstream_info(upstreams, nil) + return 200, infos +end + +function _M.dump_upstream_info() + local upstreams = get_upstreams() + local uri_segs = core.utils.split_uri(ngx_var.uri) + local upstream_id = uri_segs[4] + local upstream = iter_add_get_upstream_info(upstreams, upstream_id) + if not upstream then + return 404, {error_msg = str_format("upstream[%s] not found", upstream_id)} + end + return 200, upstream +end function _M.trigger_gc() -- TODO: find a way to trigger GC in the stream subsystem @@ -233,10 +271,22 @@ return { uris = {"/routes"}, handler = _M.dump_all_routes_info, }, - --- /v1/ + --- /v1/route/* { methods = {"GET"}, uris = {"/route/*"}, handler = _M.dump_route_info, + }, + -- /v1/upstreams + { + methods = {"GET"}, + uris = {"/upstreams"}, + handler = _M.dump_all_upstreams_info, + }, + -- /v1/upstream/* + { + methods = {"GET"}, + uris = {"/upstream/*"}, + handler = _M.dump_upstream_info, } } diff --git a/docs/en/latest/control-api.md b/docs/en/latest/control-api.md index 27697cb746a0..8829e7a03086 100644 --- a/docs/en/latest/control-api.md +++ b/docs/en/latest/control-api.md @@ -283,3 +283,83 @@ Return specific route info with **route_id** in the format below: "key": "/routes/1" } ``` + +### Get /v1/upstreams + +Introduced since `v2.11.0`. + +Dump all upstreams in the format below: + +```json +[ + { + "value":{ + "scheme":"http", + "pass_host":"pass", + "nodes":[ + { + "host":"127.0.0.1", + "port":80, + "weight":1 + }, + { + "host":"foo.com", + "port":80, + "weight":2 + } + ], + "hash_on":"vars", + "update_time":1634543819, + "key":"remote_addr", + "create_time":1634539759, + "id":"1", + "type":"chash" + }, + "has_domain":true, + "key":"\/apisix\/upstreams\/1", + "clean_handlers":{ + }, + "createdIndex":938, + "modifiedIndex":1225 + } +] +``` + +### Get /v1/upstream/{upstream_id} + +Introduced since `v2.11.0`. + +Dump specific upstream info with **upstream_id** in the format below: + +```json +{ + "value":{ + "scheme":"http", + "pass_host":"pass", + "nodes":[ + { + "host":"127.0.0.1", + "port":80, + "weight":1 + }, + { + "host":"foo.com", + "port":80, + "weight":2 + } + ], + "hash_on":"vars", + "update_time":1634543819, + "key":"remote_addr", + "create_time":1634539759, + "id":"1", + "type":"chash" + }, + "has_domain":true, + "key":"\/apisix\/upstreams\/1", + "clean_handlers":{ + }, + "createdIndex":938, + "modifiedIndex":1225 +} +``` diff --git a/t/control/upstreams.t b/t/control/upstreams.t new file mode 100644 index 000000000000..c2d76fbd2f24 --- /dev/null +++ b/t/control/upstreams.t @@ -0,0 +1,144 @@ +# +# 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) = @_; + + if (!$block->yaml_config) { + my $yaml_config = <<_EOC_; +apisix: + node_listen: 1984 + config_center: yaml + enable_admin: false +_EOC_ + + $block->set_value("yaml_config", $yaml_config); + } + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: dump all upstreams +--- apisix_yaml +upstreams: + - + id: 1 + nodes: + "127.0.0.1:8001": 1 + type: roundrobin + - + id: 2 + nodes: + "127.0.0.1:8002": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body, res = t.test('/v1/upstreams', + ngx.HTTP_GET) + res = json.decode(res) + if res[2] and table.getn(res) == 2 then + local data = {} + data.nodes = res[2].value.nodes + ngx.say(json.encode(data)) + end + } + } +--- response_body +{"nodes":[{"host":"127.0.0.1","port":8002,"weight":1}]} + + + +=== TEST 2: dump specific upstream with id 1 +--- apisix_yaml +upstreams: + - + id: 1 + nodes: + "127.0.0.1:8001": 1 + type: roundrobin + - + id: 2 + nodes: + "127.0.0.1:8002": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body, res = t.test('/v1/upstream/1', + ngx.HTTP_GET) + res = json.decode(res) + if res then + local data = {} + data.nodes = res.value.nodes + ngx.say(json.encode(data)) + end + } + } +--- response_body +{"nodes":[{"host":"127.0.0.1","port":8001,"weight":1}]} + + + +=== TEST 3: upstreams with invalid id +--- apisix_yaml +upstreams: + - + id: 1 + nodes: + "127.0.0.1:8001": 1 + type: roundrobin + - + id: 2 + nodes: + "127.0.0.1:8002": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body, res = t.test('/v1/upstream/3', + ngx.HTTP_GET) + local data = {} + data.status = code + ngx.say(json.encode(data)) + return + } + } +--- response_body +{"status":404} From e68e03f863c656a1cb38c5b19ac31e11c07ba009 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 20 Oct 2021 06:25:01 +0530 Subject: [PATCH 017/260] feat(control): expose services(#5271) --- apisix/control/v1.lua | 53 +++++++++- docs/en/latest/control-api.md | 82 +++++++++++++++ t/control/services.t | 186 ++++++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 t/control/services.t diff --git a/apisix/control/v1.lua b/apisix/control/v1.lua index 64d778a2cad4..bbe457cd607f 100644 --- a/apisix/control/v1.lua +++ b/apisix/control/v1.lua @@ -165,7 +165,7 @@ local function iter_add_get_routes_info(values, route_id) new_route.value.upstream.parent = nil end core.table.insert(infos, new_route) - -- check the roude id + -- check the route id if route_id and route.value.id == route_id then return new_route end @@ -240,6 +240,43 @@ function _M.trigger_gc() end +local function iter_add_get_services_info(values, svc_id) + local infos = {} + for _, svc in core.config_util.iterate_values(values) do + local new_svc = core.table.deepcopy(svc) + if new_svc.value.upstream and new_svc.value.upstream.parent then + new_svc.value.upstream.parent = nil + end + core.table.insert(infos, new_svc) + -- check the service id + if svc_id and svc.value.id == svc_id then + return new_svc + end + end + if not svc_id then + return infos + end + return nil +end + +function _M.dump_all_services_info() + local services = get_services() + local infos = iter_add_get_services_info(services, nil) + return 200, infos +end + +function _M.dump_service_info() + local services = get_services() + local uri_segs = core.utils.split_uri(ngx_var.uri) + local svc_id = uri_segs[4] + local info = iter_add_get_services_info(services, svc_id) + if not info then + return 404, {error_msg = str_format("service[%s] not found", svc_id)} + end + return 200, info +end + + return { -- /v1/schema { @@ -271,12 +308,24 @@ return { uris = {"/routes"}, handler = _M.dump_all_routes_info, }, - --- /v1/route/* + -- /v1/route/* { methods = {"GET"}, uris = {"/route/*"}, handler = _M.dump_route_info, }, + -- /v1/services + { + methods = {"GET"}, + uris = {"/services"}, + handler = _M.dump_all_services_info + }, + -- /v1/service/* + { + methods = {"GET"}, + uris = {"/service/*"}, + handler = _M.dump_service_info + }, -- /v1/upstreams { methods = {"GET"}, diff --git a/docs/en/latest/control-api.md b/docs/en/latest/control-api.md index 8829e7a03086..b1e6e782bc5a 100644 --- a/docs/en/latest/control-api.md +++ b/docs/en/latest/control-api.md @@ -284,6 +284,88 @@ Return specific route info with **route_id** in the format below: } ``` +### Get /v1/services + +Introduced since `v2.11`. + +Return all services info in the format below: + +```json +[ + { + "has_domain": false, + "clean_handlers": {}, + "modifiedIndex": 671, + "key": "/apisix/services/200", + "createdIndex": 671, + "value": { + "upstream": { + "scheme": "http", + "hash_on": "vars", + "pass_host": "pass", + "type": "roundrobin", + "nodes": [ + { + "port": 80, + "weight": 1, + "host": "39.97.63.215" + } + ] + }, + "create_time": 1634552648, + "id": "200", + "plugins": { + "limit-count": { + "key": "remote_addr", + "time_window": 60, + "redis_timeout": 1000, + "allow_degradation": false, + "show_limit_quota_header": true, + "policy": "local", + "count": 2, + "rejected_code": 503 + } + }, + "update_time": 1634552648 + } + } +] +``` + +### Get /v1/service/{service_id} + +Introduced since `v2.11`. + +Return specific service info with **service_id** in the format below: + +```json +{ + "has_domain": false, + "clean_handlers": {}, + "modifiedIndex": 728, + "key": "/apisix/services/5", + "createdIndex": 728, + "value": { + "create_time": 1634554563, + "id": "5", + "upstream": { + "scheme": "http", + "hash_on": "vars", + "pass_host": "pass", + "type": "roundrobin", + "nodes": [ + { + "port": 80, + "weight": 1, + "host": "39.97.63.215" + } + ] + }, + "update_time": 1634554563 + } +} +``` + ### Get /v1/upstreams Introduced since `v2.11.0`. diff --git a/t/control/services.t b/t/control/services.t new file mode 100644 index 000000000000..734afcc2b067 --- /dev/null +++ b/t/control/services.t @@ -0,0 +1,186 @@ +# +# 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) = @_; + + if (!$block->yaml_config) { + my $yaml_config = <<_EOC_; +apisix: + node_listen: 1984 + config_center: yaml + enable_admin: false +_EOC_ + + $block->set_value("yaml_config", $yaml_config); + } + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: services +--- apisix_yaml +services: + - + id: 200 + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body, res = t.test('/v1/services', + ngx.HTTP_GET) + res = json.decode(res) + if res[1] then + local data = {} + data.id = res[1].value.id + data.plugins = res[1].value.plugins + data.upstream = res[1].value.upstream + ngx.say(json.encode(data)) + end + return + } + } +--- response_body +{"id":"200","upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}} + + + +=== TEST 2: multiple services +--- apisix_yaml +services: + - + id: 200 + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + - + id: 201 + upstream: + nodes: + "127.0.0.2:1980": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local core = require("apisix.core") + local code, body, res = t.test('/v1/services', + ngx.HTTP_GET) + res = json.decode(res) + local g_data = {} + for _, r in core.config_util.iterate_values(res) do + local data = {} + data.id = r.value.id + data.plugins = r.value.plugins + data.upstream = r.value.upstream + core.table.insert(g_data, data) + end + ngx.say(json.encode(g_data)) + return + } + } +--- response_body +[{"id":"200","upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}},{"id":"201","upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.2","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}}] + + + +=== TEST 3: get service with id 5 +--- apisix_yaml +services: + - + id: 5 + plugins: + limit-count: + count: 2 + time_window: 60 + rejected_code: 503 + key: remote_addr + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body, res = t.test('/v1/service/5', + ngx.HTTP_GET) + res = json.decode(res) + if res then + local data = {} + data.id = res.value.id + data.plugins = res.value.plugins + data.upstream = res.value.upstream + ngx.say(json.encode(data)) + end + return + } + } +--- response_body +{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}} + + + +=== TEST 4: services with invalid id +--- apisix_yaml +services: + - + id: 1 + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + local code, body, res = t.test('/v1/service/2', + ngx.HTTP_GET) + local data = {} + data.status = code + ngx.say(json.encode(data)) + return + } + } +--- response_body +{"status":404} From b5d72cbf1315f1efc80689246a7dcf124c9e6443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 20 Oct 2021 08:55:15 +0800 Subject: [PATCH 018/260] chore: remove unused ASFLicenseHeaderMarkdown (#5284) --- ci/ASFLicenseHeaderMarkdown.txt | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 ci/ASFLicenseHeaderMarkdown.txt diff --git a/ci/ASFLicenseHeaderMarkdown.txt b/ci/ASFLicenseHeaderMarkdown.txt deleted file mode 100644 index c4748142bc53..000000000000 --- a/ci/ASFLicenseHeaderMarkdown.txt +++ /dev/null @@ -1,18 +0,0 @@ - From 1b964443c81ab80bd839cc1119e2257deb24c8a1 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 20 Oct 2021 08:39:48 +0530 Subject: [PATCH 019/260] docs: enabling MD045 - images with alternate text (#5280) --- .markdownlint.yml | 1 - docs/en/latest/aws.md | 10 ++++----- docs/en/latest/discovery.md | 2 +- docs/en/latest/discovery/consul_kv.md | 2 +- docs/en/latest/plugins/hmac-auth.md | 6 ++++-- docs/en/latest/plugins/jwt-auth.md | 9 +++++--- docs/en/latest/plugins/key-auth.md | 4 ++-- docs/en/latest/plugins/limit-conn.md | 2 +- docs/en/latest/plugins/prometheus.md | 16 +++++++-------- docs/en/latest/plugins/response-rewrite.md | 2 +- docs/en/latest/plugins/sls-logger.md | 2 +- docs/en/latest/plugins/wolf-rbac.md | 4 ++-- docs/en/latest/plugins/zipkin.md | 7 ++++--- docs/zh/latest/README.md | 4 ++-- docs/zh/latest/discovery.md | 24 +++++++++++----------- docs/zh/latest/plugins/jwt-auth.md | 7 ++++--- docs/zh/latest/plugins/key-auth.md | 4 ++-- docs/zh/latest/plugins/limit-conn.md | 2 +- docs/zh/latest/plugins/prometheus.md | 16 +++++++-------- docs/zh/latest/plugins/response-rewrite.md | 2 +- docs/zh/latest/plugins/sls-logger.md | 2 +- docs/zh/latest/plugins/wolf-rbac.md | 4 ++-- docs/zh/latest/plugins/zipkin.md | 6 +++--- 23 files changed, 72 insertions(+), 66 deletions(-) diff --git a/.markdownlint.yml b/.markdownlint.yml index 3836e57bc911..36d248569285 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -31,5 +31,4 @@ MD034: false MD036: false MD040: false MD041: false -MD045: false MD046: false diff --git a/docs/en/latest/aws.md b/docs/en/latest/aws.md index 07a0e7d84586..ce9636c0dc9d 100644 --- a/docs/en/latest/aws.md +++ b/docs/en/latest/aws.md @@ -27,7 +27,7 @@ title: Running APISIX in AWS with AWS CDK This reference architecture walks you through building **APISIX** as a serverless container API Gateway on top of AWS Fargate with AWS CDK. -![](../../assets/images/aws-fargate-cdk.png) +![Apache APISIX Serverless Architecture](../../assets/images/aws-fargate-cdk.png) ## Generate an AWS CDK project with `projen` @@ -208,15 +208,15 @@ Address: 44.226.102.63 Configure the IP addresses returned as your upstream nodes in your **APISIX** dashboard followed by the **Services** and **Routes** configuration. Let's say we have a `/index.php` as the URI for the first route for our first **Service** from the **Upstream** IP addresses. -![](../../assets/images/aws-nlb-ip-addr.png) -![](../../assets/images/aws-define-service.png) -![](../../assets/images/aws-define-route.png) +![upstream with AWS NLB IP addresses](../../assets/images/aws-nlb-ip-addr.png) +![service with created upstream](../../assets/images/aws-define-service.png) +![define route with service and uri](../../assets/images/aws-define-route.png) ## Validation OK. Let's test the `/index.php` on `{apiSix.ApiSixServiceServiceURL}/index.php` -![](../../assets/images/aws-caddy-php-welcome-page.png) +![Testing Apache APISIX on AWS Fargate](../../assets/images/aws-caddy-php-welcome-page.png) Now we have been successfully running **APISIX** in AWS Fargate as serverless container API Gateway service. diff --git a/docs/en/latest/discovery.md b/docs/en/latest/discovery.md index 6565178b463f..89f7ef171eff 100644 --- a/docs/en/latest/discovery.md +++ b/docs/en/latest/discovery.md @@ -25,7 +25,7 @@ title: Integration service discovery registry When system traffic changes, the number of servers of the upstream service also increases or decreases, or the server needs to be replaced due to its hardware failure. If the gateway maintains upstream service information through configuration, the maintenance costs in the microservices architecture pattern are unpredictable. Furthermore, due to the untimely update of these information, will also bring a certain impact for the business, and the impact of human error operation can not be ignored. So it is very necessary for the gateway to automatically get the latest list of service instances through the service registry。As shown in the figure below: -![](../../assets/images/discovery.png) +![discovery through service registry](../../assets/images/discovery.png) 1. When the service starts, it will report some of its information, such as the service name, IP, port and other information to the registry. The services communicate with the registry using a mechanism such as a heartbeat, and if the registry and the service are unable to communicate for a long time, the instance will be cancel.When the service goes offline, the registry will delete the instance information. 2. The gateway gets service instance information from the registry in near-real time. diff --git a/docs/en/latest/discovery/consul_kv.md b/docs/en/latest/discovery/consul_kv.md index f7291543c81f..830369e861c1 100644 --- a/docs/en/latest/discovery/consul_kv.md +++ b/docs/en/latest/discovery/consul_kv.md @@ -26,7 +26,7 @@ title: consul_kv For users who used [nginx-upsync-module](https://github.com/weibocom/nginx-upsync-module) and consul key value for service discovery way, as we Weibo Mobile Team, maybe need it. Thanks to @fatman-x guy, who developed this module, called `consul_kv`, and its worker process data flow is below: -![](https://user-images.githubusercontent.com/548385/107141841-6ced3e00-6966-11eb-8aa4-bc790a4ad113.png) +![consul kv module data flow diagram](https://user-images.githubusercontent.com/548385/107141841-6ced3e00-6966-11eb-8aa4-bc790a4ad113.png) ## Configuration for discovery client diff --git a/docs/en/latest/plugins/hmac-auth.md b/docs/en/latest/plugins/hmac-auth.md index 234609694059..7a23cfff6e82 100644 --- a/docs/en/latest/plugins/hmac-auth.md +++ b/docs/en/latest/plugins/hmac-auth.md @@ -29,8 +29,10 @@ title: hmac-auth - [How To Enable](#how-to-enable) - [Test Plugin](#test-plugin) - [generate signature:](#generate-signature) + - [Request body checking](#request-body-checking) - [Use the generated signature to try the request](#use-the-generated-signature-to-try-the-request) - [Custom header key](#custom-header-key) + - [Enable request body checking](#enable-request-body-checking) - [Disable Plugin](#disable-plugin) - [Generate Signature Examples](#generate-signature-examples) @@ -76,10 +78,10 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 The default `keep_headers` is false and `encode_uri_params` is true. You can visit the dashboard to complete the above operations through the web interface, first add a consumer: -![](../../../assets/images/plugin/hmac-auth-1.png) +![create a consumer](../../../assets/images/plugin/hmac-auth-1.png) Then add the hmac-auth plugin to the consumer page: -![](../../../assets/images/plugin/hmac-auth-2.png) +![enable hmac plugin](../../../assets/images/plugin/hmac-auth-2.png) 2. add a Route or add a Service, and enable the `hmac-auth` plugin diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index b3cbfc59828c..bc16d4add10e 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -25,8 +25,11 @@ title: jwt-auth - [**Name**](#name) - [**Attributes**](#attributes) +- [**API**](#api) - [**How To Enable**](#how-to-enable) - [**Test Plugin**](#test-plugin) + - [get the token in `jwt-auth` plugin:](#get-the-token-in-jwt-auth-plugin) + - [try request with token](#try-request-with-token) - [**Disable Plugin**](#disable-plugin) ## Name @@ -111,14 +114,14 @@ You can use [APISIX Dashboard](https://github.com/apache/apisix-dashboard) to co 1. Add a Consumer through the web console: -![](../../../assets/images/plugin/jwt-auth-1.png) +![create a consumer](../../../assets/images/plugin/jwt-auth-1.png) then add jwt-auth plugin in the Consumer page: -![](../../../assets/images/plugin/jwt-auth-2.png) +![enable jwt plugin](../../../assets/images/plugin/jwt-auth-2.png) 2. Create a Route or Service object and enable the jwt-auth plugin: -![](../../../assets/images/plugin/jwt-auth-3.png) +![enable jwt from route or service](../../../assets/images/plugin/jwt-auth-3.png) ## Test Plugin diff --git a/docs/en/latest/plugins/key-auth.md b/docs/en/latest/plugins/key-auth.md index aaea7061b8d9..25fec5f1308f 100644 --- a/docs/en/latest/plugins/key-auth.md +++ b/docs/en/latest/plugins/key-auth.md @@ -69,10 +69,10 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 ``` You also can complete the above operation through the web interface, first add a route: -![](../../../assets/images/plugin/key-auth-1.png) +![create a consumer](../../../assets/images/plugin/key-auth-1.png) Then add key-auth plugin: -![](../../../assets/images/plugin/key-auth-2.png) +![enable key-auth plugin](../../../assets/images/plugin/key-auth-2.png) 2. creates a route or service object, and enable plugin `key-auth`. diff --git a/docs/en/latest/plugins/limit-conn.md b/docs/en/latest/plugins/limit-conn.md index 2a468a6e9b2c..dc01c74ede8d 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -78,7 +78,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 ``` You also can complete the above operation through the web interface, first add a route, then add limit-conn plugin: -![](../../../assets/images/plugin/limit-conn-1.png) +![enable limit-conn plugin](../../../assets/images/plugin/limit-conn-1.png) ## Test Plugin diff --git a/docs/en/latest/plugins/prometheus.md b/docs/en/latest/plugins/prometheus.md index ee3073bbe5b2..2a4eaf6ce0f6 100644 --- a/docs/en/latest/plugins/prometheus.md +++ b/docs/en/latest/plugins/prometheus.md @@ -84,11 +84,11 @@ You can use [APISIX Dashboard](https://github.com/apache/apisix-dashboard) to co First, add a Route: -![](../../../assets/images/plugin/prometheus-1.png) +![create a route](../../../assets/images/plugin/prometheus-1.png) Then add prometheus plugin: -![](../../../assets/images/plugin/prometheus-2.png) +![enable prometheus plugin](../../../assets/images/plugin/prometheus-2.png) ## How to fetch the metric data @@ -114,9 +114,9 @@ scrape_configs: And we can check the status at prometheus console: -![](../../../assets/images/plugin/prometheus01.png) +![checking status on prometheus dashboard](../../../assets/images/plugin/prometheus01.png) -![](../../../assets/images/plugin/prometheus02.png) +![prometheus apisix in-depth metric view](../../../assets/images/plugin/prometheus02.png) ## How to specify export uri @@ -142,13 +142,13 @@ Downloads [Grafana dashboard meta](https://github.com/apache/apisix/blob/master/ Or you can goto [Grafana official](https://grafana.com/grafana/dashboards/11719) for `Grafana` meta data. -![](../../../assets/images/plugin/grafana-1.png) +![Grafana chart-1](../../../assets/images/plugin/grafana-1.png) -![](../../../assets/images/plugin/grafana-2.png) +![Grafana chart-2](../../../assets/images/plugin/grafana-2.png) -![](../../../assets/images/plugin/grafana-3.png) +![Grafana chart-3](../../../assets/images/plugin/grafana-3.png) -![](../../../assets/images/plugin/grafana-4.png) +![Grafana chart-4](../../../assets/images/plugin/grafana-4.png) ### Available metrics diff --git a/docs/en/latest/plugins/response-rewrite.md b/docs/en/latest/plugins/response-rewrite.md index 37da0dd0fb00..c03ac9ba6514 100644 --- a/docs/en/latest/plugins/response-rewrite.md +++ b/docs/en/latest/plugins/response-rewrite.md @@ -131,6 +131,6 @@ The `response-rewrite` plugin has been disabled now. It works for other plugins. `ngx.exit` will interrupt the execution of the current request and return status code to Nginx. -![](https://cdn.jsdelivr.net/gh/Miss-you/img/picgo/20201113010623.png) +![ngx.edit tabular overview](https://cdn.jsdelivr.net/gh/Miss-you/img/picgo/20201113010623.png) However, if you execute `ngx.exit` during the access phase, it only interrupts the request processing phase, and the response phase will still process it, i.e. if you configure the `response-rewrite` plugin, it will force overwriting of your response information (e.g. response status code). diff --git a/docs/en/latest/plugins/sls-logger.md b/docs/en/latest/plugins/sls-logger.md index bc3834ac5a2c..24dbed75c4b0 100644 --- a/docs/en/latest/plugins/sls-logger.md +++ b/docs/en/latest/plugins/sls-logger.md @@ -99,7 +99,7 @@ hello, world * check log in ali cloud log service -![](../../../assets/images/plugin/sls-logger-1.png "sls logger view") +![sls logger view](../../../assets/images/plugin/sls-logger-1.png "sls logger view") ## Disable Plugin diff --git a/docs/en/latest/plugins/wolf-rbac.md b/docs/en/latest/plugins/wolf-rbac.md index 839946593fbd..c76310e85fac 100644 --- a/docs/en/latest/plugins/wolf-rbac.md +++ b/docs/en/latest/plugins/wolf-rbac.md @@ -82,10 +82,10 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f ``` You also can complete the above operations through the web interface, first add a consumer: -![](../../../assets/images/plugin/wolf-rbac-1.png) +![add a consumer](../../../assets/images/plugin/wolf-rbac-1.png) Then add the wolf-rbac plugin to the consumer page: -![](../../../assets/images/plugin/wolf-rbac-2.png) +![enable wolf-rbac plugin](../../../assets/images/plugin/wolf-rbac-2.png) Notes: The `appid` filled in above needs to already exist in the wolf system. diff --git a/docs/en/latest/plugins/zipkin.md b/docs/en/latest/plugins/zipkin.md index 6a0c1aaa418e..d1542c69cc59 100644 --- a/docs/en/latest/plugins/zipkin.md +++ b/docs/en/latest/plugins/zipkin.md @@ -27,6 +27,7 @@ title: Zipkin - [**Attributes**](#attributes) - [**How To Enable**](#how-to-enable) - [**Test Plugin**](#test-plugin) + - [run the Zipkin instance](#run-the-zipkin-instance) - [**Disable Plugin**](#disable-plugin) ## Name @@ -95,7 +96,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 You also can complete the above operation through the web interface, first add a route, then add zipkin plugin: -![](../../../assets/images/plugin/zipkin-1.png) +![enable zipkin plugin](../../../assets/images/plugin/zipkin-1.png) ## Test Plugin @@ -121,9 +122,9 @@ Then you can use a browser to access the webUI of Zipkin: http://127.0.0.1:9411/zipkin ``` -![](../../../assets/images/plugin/zipkin-1.jpg) +![zipkin web-ui](../../../assets/images/plugin/zipkin-1.jpg) -![](../../../assets/images/plugin/zipkin-2.jpg) +![zipkin web-ui list view](../../../assets/images/plugin/zipkin-2.jpg) ## Disable Plugin diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index d0eed94dd478..c8ae97bf9fab 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -54,9 +54,9 @@ Apache APISIX 的技术架构如下图所示: - [Apache APISIX® Go Plugin Runner](https://github.com/apache/apisix-go-plugin-runner/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) - [Apache APISIX® Python Plugin Runner](https://github.com/apache/apisix-python-plugin-runner/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) - **微信公众号** -
![](../../assets/images/OA.jpg) +
![wechat official account](../../assets/images/OA.jpg) - **微信视频号** -
![](../../assets/images/MA.jpeg) +
![wechat video account](../../assets/images/MA.jpeg) ## 特性 diff --git a/docs/zh/latest/discovery.md b/docs/zh/latest/discovery.md index 73907c95b319..0466cb8bd6da 100644 --- a/docs/zh/latest/discovery.md +++ b/docs/zh/latest/discovery.md @@ -21,23 +21,23 @@ title: 集成服务发现注册中心 # --> -* [摘要](#摘要) -* [当前支持的注册中心](#当前支持的注册中心) -* [如何扩展注册中心?](#如何扩展注册中心) - * [基本步骤](#基本步骤) - * [以 Eureka 举例](#以-eureka-举例) - * [实现 eureka.lua](#实现-eurekalua) - * [Eureka 与 APISIX 之间数据转换逻辑](#eureka-与-apisix-之间数据转换逻辑) -* [注册中心配置](#注册中心配置) - * [初始化服务发现](#初始化服务发现) - * [Eureka 的配置](#eureka-的配置) -* [upstream 配置](#upstream-配置) +- [摘要](#摘要) +- [当前支持的注册中心](#当前支持的注册中心) +- [如何扩展注册中心?](#如何扩展注册中心) + - [基本步骤](#基本步骤) + - [以 Eureka 举例](#以-eureka-举例) + - [实现 eureka.lua](#实现-eurekalua) + - [Eureka 与 APISIX 之间数据转换逻辑](#eureka-与-apisix-之间数据转换逻辑) +- [注册中心配置](#注册中心配置) + - [初始化服务发现](#初始化服务发现) + - [Eureka 的配置](#eureka-的配置) +- [upstream 配置](#upstream-配置) ## 摘要 当业务量发生变化时,需要对上游服务进行扩缩容,或者因服务器硬件故障需要更换服务器。如果网关是通过配置来维护上游服务信息,在微服务架构模式下,其带来的维护成本可想而知。再者因不能及时更新这些信息,也会对业务带来一定的影响,还有人为误操作带来的影响也不可忽视,所以网关非常必要通过服务注册中心动态获取最新的服务实例信息。架构图如下所示: -![](../../assets/images/discovery-cn.png) +![discovery through service registry](../../assets/images/discovery-cn.png) 1. 服务启动时将自身的一些信息,比如服务名、IP、端口等信息上报到注册中心;各个服务与注册中心使用一定机制(例如心跳)通信,如果注册中心与服务长时间无法通信,就会注销该实例;当服务下线时,会删除注册中心的实例信息; 2. 网关会准实时地从注册中心获取服务实例信息; diff --git a/docs/zh/latest/plugins/jwt-auth.md b/docs/zh/latest/plugins/jwt-auth.md index 1e6b2534f474..ae484dceae81 100644 --- a/docs/zh/latest/plugins/jwt-auth.md +++ b/docs/zh/latest/plugins/jwt-auth.md @@ -110,14 +110,15 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 你可以使用 [APISIX Dashboard](https://github.com/apache/apisix-dashboard),通过 web 界面来完成上面的操作。 1. 先增加一个 consumer: -![](../../../assets/images/plugin/jwt-auth-1.png) + +![create a consumer](../../../assets/images/plugin/jwt-auth-1.png) 然后在 consumer 页面中添加 jwt-auth 插件: -![](../../../assets/images/plugin/jwt-auth-2.png) +![enable jwt plugin](../../../assets/images/plugin/jwt-auth-2.png) 2. 创建 Route 或 Service 对象,并开启 jwt-auth 插件: -![](../../../assets/images/plugin/jwt-auth-3.png) +![enabe jwt from route or service](../../../assets/images/plugin/jwt-auth-3.png) ## 测试插件 diff --git a/docs/zh/latest/plugins/key-auth.md b/docs/zh/latest/plugins/key-auth.md index a36e7fbd7b79..48549c49f61c 100644 --- a/docs/zh/latest/plugins/key-auth.md +++ b/docs/zh/latest/plugins/key-auth.md @@ -67,10 +67,10 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 ``` 你也可以通过 web 界面来完成上面的操作,先增加一个 consumer: -![](../../../assets/images/plugin/key-auth-1.png) +![create a consumer](../../../assets/images/plugin/key-auth-1.png) 然后在 consumer 页面中添加 key-auth 插件: -![](../../../assets/images/plugin/key-auth-2.png) +![enable key-auth plugin](../../../assets/images/plugin/key-auth-2.png) 2. 创建 route 或 service 对象,并开启 `key-auth` 插件。 diff --git a/docs/zh/latest/plugins/limit-conn.md b/docs/zh/latest/plugins/limit-conn.md index 2ff5f73618ad..5f66480dfe5f 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -69,7 +69,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 ``` 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-conn 插件: -![](../../../assets/images/plugin/limit-conn-1.png) +![enable limit-conn plugin](../../../assets/images/plugin/limit-conn-1.png) #### test plugin diff --git a/docs/zh/latest/plugins/prometheus.md b/docs/zh/latest/plugins/prometheus.md index 3c358797c197..addf30eb5600 100644 --- a/docs/zh/latest/plugins/prometheus.md +++ b/docs/zh/latest/plugins/prometheus.md @@ -84,11 +84,11 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 先增加一个 Route: -![](../../../assets/images/plugin/prometheus-1.png) +![create a route](../../../assets/images/plugin/prometheus-1.png) 然后在 route 页面中添加 prometheus 插件: -![](../../../assets/images/plugin/prometheus-2.png) +![enable prometheus plugin](../../../assets/images/plugin/prometheus-2.png) ## 如何提取指标数据 @@ -113,9 +113,9 @@ scrape_configs: 我们也可以在 prometheus 控制台中去检查状态: -![](../../../assets/images/plugin/prometheus01.png) +![checking status on prometheus dashboard](../../../assets/images/plugin/prometheus01.png) -![](../../../assets/images/plugin/prometheus02.png) +![prometheus apisix in-depth metric view](../../../assets/images/plugin/prometheus02.png) ## 如何修改暴露指标的 uri @@ -141,13 +141,13 @@ plugin_attr: 你可以到 [Grafana 官方](https://grafana.com/grafana/dashboards/11719) 下载 `Grafana` 元数据. -![](../../../assets/images/plugin/grafana-1.png) +![Grafana chart-1](../../../assets/images/plugin/grafana-1.png) -![](../../../assets/images/plugin/grafana-2.png) +![Grafana chart-2](../../../assets/images/plugin/grafana-2.png) -![](../../../assets/images/plugin/grafana-3.png) +![Grafana chart-3](../../../assets/images/plugin/grafana-3.png) -![](../../../assets/images/plugin/grafana-4.png) +![Grafana chart-4](../../../assets/images/plugin/grafana-4.png) ### 可有的指标 diff --git a/docs/zh/latest/plugins/response-rewrite.md b/docs/zh/latest/plugins/response-rewrite.md index cf2facf9179a..36758d143e5a 100644 --- a/docs/zh/latest/plugins/response-rewrite.md +++ b/docs/zh/latest/plugins/response-rewrite.md @@ -125,6 +125,6 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 `ngx.exit`将中断当前请求的执行,并返回状态码给 Nginx。 -![](https://cdn.jsdelivr.net/gh/Miss-you/img/picgo/20201113010623.png) +![ngx.edit tabular overview](https://cdn.jsdelivr.net/gh/Miss-you/img/picgo/20201113010623.png) 但是很多人可能会对`ngx.exit`理解出现偏差,即如果你在`access`阶段执行`ngx.exit`,只是中断了请求处理阶段,响应阶段仍然会处理。比如,如果你配置了`response-rewrite`插件,它会强制覆盖你的响应信息(如响应代码)。 diff --git a/docs/zh/latest/plugins/sls-logger.md b/docs/zh/latest/plugins/sls-logger.md index 902d7449b55f..c69f35849ae4 100644 --- a/docs/zh/latest/plugins/sls-logger.md +++ b/docs/zh/latest/plugins/sls-logger.md @@ -101,7 +101,7 @@ hello, world ``` * 查看阿里云日志服务上传记录 -![](../../../assets/images/plugin/sls-logger-1.png "阿里云日志服务预览") +![sls logger view](../../../assets/images/plugin/sls-logger-1.png "阿里云日志服务预览") ## 禁用插件 diff --git a/docs/zh/latest/plugins/wolf-rbac.md b/docs/zh/latest/plugins/wolf-rbac.md index ee81c8c66385..a176e0fea1ba 100644 --- a/docs/zh/latest/plugins/wolf-rbac.md +++ b/docs/zh/latest/plugins/wolf-rbac.md @@ -82,10 +82,10 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f ``` 你也可以通过 web 界面来完成上面的操作,先增加一个 consumer: -![](../../../assets/images/plugin/wolf-rbac-1.png) +![add a consumer](../../../assets/images/plugin/wolf-rbac-1.png) 然后在 consumer 页面中添加 wolf-rbac 插件: -![](../../../assets/images/plugin/wolf-rbac-2.png) +![enable wolf-rbac plugin](../../../assets/images/plugin/wolf-rbac-2.png) 注意: 上面填写的 `appid` 需要在 wolf 控制台中已经存在的. diff --git a/docs/zh/latest/plugins/zipkin.md b/docs/zh/latest/plugins/zipkin.md index 44a7b9145d6d..831b5041bcbb 100644 --- a/docs/zh/latest/plugins/zipkin.md +++ b/docs/zh/latest/plugins/zipkin.md @@ -95,7 +95,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 zipkin 插件: -![](../../../assets/images/plugin/zipkin-1.png) +![enable zipkin plugin](../../../assets/images/plugin/zipkin-1.png) ## 测试插件 @@ -121,9 +121,9 @@ HTTP/1.1 200 OK http://127.0.0.1:9411/zipkin ``` -![](../../../assets/images/plugin/zipkin-1.jpg) +![zipkin web-ui](../../../assets/images/plugin/zipkin-1.jpg) -![](../../../assets/images/plugin/zipkin-2.jpg) +![zipkin web-ui list view](../../../assets/images/plugin/zipkin-2.jpg) ## 禁用插件 From 4dd1f6799f8207ed2f07da0e2a34c282b26cd877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 21 Oct 2021 07:52:16 +0800 Subject: [PATCH 020/260] chore: we should release the 2.10.x versions from 2.10 branch (#5297) Signed-off-by: spacewander --- .asf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index 266232566505..152a7207de4a 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -47,7 +47,7 @@ github: required_pull_request_reviews: require_code_owner_reviews: true required_approving_review_count: 2 - release/2.10.0: + release/2.10: required_pull_request_reviews: require_code_owner_reviews: true required_approving_review_count: 2 From 71a2259d1561fa8bbd0bc322054d2e004e1a797e Mon Sep 17 00:00:00 2001 From: agile6v Date: Thu, 21 Oct 2021 15:57:54 +0800 Subject: [PATCH 021/260] feat(proxy-cache): support memory-based strategy (#5028) --- Makefile | 3 + apisix/cli/ngx_tpl.lua | 6 + apisix/cli/ops.lua | 40 ++ apisix/plugins/proxy-cache.lua | 288 -------- apisix/plugins/proxy-cache/disk_handler.lua | 97 +++ apisix/plugins/proxy-cache/init.lua | 188 +++++ apisix/plugins/proxy-cache/memory.lua | 70 ++ apisix/plugins/proxy-cache/memory_handler.lua | 326 +++++++++ apisix/plugins/proxy-cache/util.lua | 102 +++ conf/config-default.yaml | 17 +- docs/en/latest/plugins/proxy-cache.md | 3 + docs/zh/latest/plugins/proxy-cache.md | 3 + t/cli/test_main.sh | 6 +- .../{proxy-cache.t => proxy-cache/disk.t} | 59 +- t/plugin/proxy-cache/memory.t | 667 ++++++++++++++++++ 15 files changed, 1525 insertions(+), 350 deletions(-) delete mode 100644 apisix/plugins/proxy-cache.lua create mode 100644 apisix/plugins/proxy-cache/disk_handler.lua create mode 100644 apisix/plugins/proxy-cache/init.lua create mode 100644 apisix/plugins/proxy-cache/memory.lua create mode 100644 apisix/plugins/proxy-cache/memory_handler.lua create mode 100644 apisix/plugins/proxy-cache/util.lua rename t/plugin/{proxy-cache.t => proxy-cache/disk.t} (96%) create mode 100644 t/plugin/proxy-cache/memory.t diff --git a/Makefile b/Makefile index c5b548360d34..002ed2e8a6b2 100644 --- a/Makefile +++ b/Makefile @@ -309,6 +309,9 @@ install: runtime $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/prometheus $(INSTALL) apisix/plugins/prometheus/*.lua $(INST_LUADIR)/apisix/plugins/prometheus/ + $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/proxy-cache + $(INSTALL) apisix/plugins/proxy-cache/*.lua $(INST_LUADIR)/apisix/plugins/proxy-cache/ + $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/serverless $(INSTALL) apisix/plugins/serverless/*.lua $(INST_LUADIR)/apisix/plugins/serverless/ diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 6b78764f875f..80760b7d9159 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -209,7 +209,11 @@ http { {% if enabled_plugins["proxy-cache"] then %} # for proxy cache {% for _, cache in ipairs(proxy_cache.zones) do %} + {% if cache.disk_path and cache.cache_levels and cache.disk_size then %} proxy_cache_path {* cache.disk_path *} levels={* cache.cache_levels *} keys_zone={* cache.name *}:{* cache.memory_size *} inactive=1d max_size={* cache.disk_size *} use_temp_path=off; + {% else %} + lua_shared_dict {* cache.name *} {* cache.memory_size *}; + {% end %} {% end %} {% end %} @@ -217,8 +221,10 @@ http { # for proxy cache map $upstream_cache_zone $upstream_cache_zone_info { {% for _, cache in ipairs(proxy_cache.zones) do %} + {% if cache.disk_path and cache.cache_levels and cache.disk_size then %} {* cache.name *} {* cache.disk_path *},{* cache.cache_levels *}; {% end %} + {% end %} } {% end %} diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 750f5b1e0dfe..68568680b57b 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -174,6 +174,46 @@ local config_schema = { }, } }, + proxy_cache = { + type = "object", + properties = { + zones = { + type = "array", + minItems = 1, + items = { + type = "object", + properties = { + name = { + type = "string", + }, + memory_size = { + type = "string", + }, + disk_size = { + type = "string", + }, + disk_path = { + type = "string", + }, + cache_levels = { + type = "string", + }, + }, + oneOf = { + { + required = {"name", "memory_size"}, + maxProperties = 2, + }, + { + required = {"name", "memory_size", "disk_size", + "disk_path", "cache_levels"}, + } + }, + }, + uniqueItems = true, + } + } + }, port_admin = { type = "integer", }, diff --git a/apisix/plugins/proxy-cache.lua b/apisix/plugins/proxy-cache.lua deleted file mode 100644 index 34f30569d9aa..000000000000 --- a/apisix/plugins/proxy-cache.lua +++ /dev/null @@ -1,288 +0,0 @@ --- --- Licensed to the Apache Software Foundation (ASF) under one or more --- contributor license agreements. See the NOTICE file distributed with --- this work for additional information regarding copyright ownership. --- The ASF licenses this file to You under the Apache License, Version 2.0 --- (the "License"); you may not use this file except in compliance with --- the License. You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. --- - -local core = require("apisix.core") -local ngx_re = require("ngx.re") -local tab_concat = table.concat -local string = string -local io_open = io.open -local io_close = io.close -local ngx = ngx -local os = os -local ipairs = ipairs -local pairs = pairs -local tonumber = tonumber - -local plugin_name = "proxy-cache" - -local schema = { - type = "object", - properties = { - cache_zone = { - type = "string", - minLength = 1, - maxLength = 100, - default = "disk_cache_one", - }, - cache_key = { - type = "array", - minItems = 1, - items = { - description = "a key for caching", - type = "string", - pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]], - }, - default = {"$host", "$request_uri"} - }, - cache_http_status = { - type = "array", - minItems = 1, - items = { - description = "http response status", - type = "integer", - minimum = 200, - maximum = 599, - }, - uniqueItems = true, - default = {200, 301, 404}, - }, - cache_method = { - type = "array", - minItems = 1, - items = { - description = "supported http method", - type = "string", - enum = {"GET", "POST", "HEAD"}, - }, - uniqueItems = true, - default = {"GET", "HEAD"}, - }, - hide_cache_headers = { - type = "boolean", - default = false, - }, - cache_bypass = { - type = "array", - minItems = 1, - items = { - type = "string", - pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]] - }, - }, - no_cache = { - type = "array", - minItems = 1, - items = { - type = "string", - pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]] - }, - }, - }, -} - -local _M = { - version = 0.1, - priority = 1009, - name = plugin_name, - schema = schema, -} - - -function _M.check_schema(conf) - local ok, err = core.schema.check(schema, conf) - if not ok then - return false, err - end - - for _, key in ipairs(conf.cache_key) do - if key == "$request_method" then - return false, "cache_key variable " .. key .. " unsupported" - end - end - - local found = false - local local_conf = core.config.local_conf() - if local_conf.apisix.proxy_cache then - for _, cache in ipairs(local_conf.apisix.proxy_cache.zones) do - if cache.name == conf.cache_zone then - found = true - end - end - - if found == false then - return false, "cache_zone " .. conf.cache_zone .. " not found" - end - end - return true -end - - -local tmp = {} -local function generate_complex_value(data, ctx) - core.table.clear(tmp) - - core.log.info("proxy-cache complex value: ", core.json.delay_encode(data)) - for i, value in ipairs(data) do - core.log.info("proxy-cache complex value index-", i, ": ", value) - - if string.byte(value, 1, 1) == string.byte('$') then - tmp[i] = ctx.var[string.sub(value, 2)] - else - tmp[i] = value - end - end - - return tab_concat(tmp, "") -end - - --- check whether the request method and response status --- match the user defined. -local function match_method_and_status(conf, ctx) - local match_method, match_status = false, false - - -- Maybe there is no need for optimization here. - for _, method in ipairs(conf.cache_method) do - if method == ctx.var.request_method then - match_method = true - break - end - end - - for _, status in ipairs(conf.cache_http_status) do - if status == ngx.status then - match_status = true - break - end - end - - if match_method and match_status then - return true - end - - return false -end - - -local function file_exists(name) - local f = io_open(name, "r") - if f ~= nil then - io_close(f) - return true - end - return false -end - - -local function generate_cache_filename(cache_path, cache_levels, cache_key) - local md5sum = ngx.md5(cache_key) - local levels = ngx_re.split(cache_levels, ":") - local filename = "" - - local index = #md5sum - for k, v in pairs(levels) do - local length = tonumber(v) - index = index - length - filename = filename .. md5sum:sub(index+1, index+length) .. "/" - end - if cache_path:sub(-1) ~= "/" then - cache_path = cache_path .. "/" - end - filename = cache_path .. filename .. md5sum - return filename -end - - -local function cache_purge(conf, ctx) - local cache_zone_info = ngx_re.split(ctx.var.upstream_cache_zone_info, ",") - - local filename = generate_cache_filename(cache_zone_info[1], cache_zone_info[2], - ctx.var.upstream_cache_key) - if file_exists(filename) then - os.remove(filename) - return nil - end - - return "Not found" -end - - -function _M.rewrite(conf, ctx) - core.log.info("proxy-cache plugin rewrite phase, conf: ", core.json.delay_encode(conf)) - - ctx.var.upstream_cache_zone = conf.cache_zone - - local value = generate_complex_value(conf.cache_key, ctx) - ctx.var.upstream_cache_key = value - core.log.info("proxy-cache cache key value:", value) - - if ctx.var.request_method == "PURGE" then - local err = cache_purge(conf, ctx) - if err ~= nil then - return 404 - end - - return 200 - end - - if conf.cache_bypass ~= nil then - local value = generate_complex_value(conf.cache_bypass, ctx) - ctx.var.upstream_cache_bypass = value - core.log.info("proxy-cache cache bypass value:", value) - end -end - - -function _M.header_filter(conf, ctx) - core.log.info("proxy-cache plugin header filter phase, conf: ", core.json.delay_encode(conf)) - - local no_cache = "1" - - if match_method_and_status(conf, ctx) then - no_cache = "0" - end - - if conf.no_cache ~= nil then - local value = generate_complex_value(conf.no_cache, ctx) - core.log.info("proxy-cache no-cache value:", value) - - if value ~= nil and value ~= "" and value ~= "0" then - no_cache = "1" - end - end - - local upstream_hdr_cache_control - local upstream_hdr_expires - - if conf.hide_cache_headers == true then - upstream_hdr_cache_control = "" - upstream_hdr_expires = "" - else - upstream_hdr_cache_control = ctx.var.upstream_http_cache_control - upstream_hdr_expires = ctx.var.upstream_http_expires - end - - core.response.set_header("Cache-Control", upstream_hdr_cache_control, - "Expires", upstream_hdr_expires, - "Apisix-Cache-Status", ctx.var.upstream_cache_status) - - ctx.var.upstream_no_cache = no_cache - core.log.info("proxy-cache no cache:", no_cache) -end - - -return _M diff --git a/apisix/plugins/proxy-cache/disk_handler.lua b/apisix/plugins/proxy-cache/disk_handler.lua new file mode 100644 index 000000000000..bf131b158a8c --- /dev/null +++ b/apisix/plugins/proxy-cache/disk_handler.lua @@ -0,0 +1,97 @@ +-- +-- 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 os = os +local ngx_re = require("ngx.re") +local core = require("apisix.core") +local util = require("apisix.plugins.proxy-cache.util") + +local _M = {} + + +local function disk_cache_purge(conf, ctx) + local cache_zone_info = ngx_re.split(ctx.var.upstream_cache_zone_info, ",") + + local filename = util.generate_cache_filename(cache_zone_info[1], cache_zone_info[2], + ctx.var.upstream_cache_key) + + if util.file_exists(filename) then + os.remove(filename) + return nil + end + + return "Not found" +end + + +function _M.access(conf, ctx) + ctx.var.upstream_cache_zone = conf.cache_zone + + if ctx.var.request_method == "PURGE" then + local err = disk_cache_purge(conf, ctx) + if err ~= nil then + return 404 + end + + return 200 + end + + if conf.cache_bypass ~= nil then + local value = util.generate_complex_value(conf.cache_bypass, ctx) + ctx.var.upstream_cache_bypass = value + core.log.info("proxy-cache cache bypass value:", value) + end +end + + +function _M.header_filter(conf, ctx) + local no_cache = "1" + + if util.match_method(conf, ctx) and util.match_status(conf, ctx) then + no_cache = "0" + end + + if conf.no_cache ~= nil then + local value = util.generate_complex_value(conf.no_cache, ctx) + core.log.info("proxy-cache no-cache value:", value) + + if value ~= nil and value ~= "" and value ~= "0" then + no_cache = "1" + end + end + + local upstream_hdr_cache_control + local upstream_hdr_expires + + if conf.hide_cache_headers == true then + upstream_hdr_cache_control = "" + upstream_hdr_expires = "" + else + upstream_hdr_cache_control = ctx.var.upstream_http_cache_control + upstream_hdr_expires = ctx.var.upstream_http_expires + end + + core.response.set_header("Cache-Control", upstream_hdr_cache_control, + "Expires", upstream_hdr_expires, + "Apisix-Cache-Status", ctx.var.upstream_cache_status) + + ctx.var.upstream_no_cache = no_cache + core.log.info("proxy-cache no cache:", no_cache) +end + + +return _M diff --git a/apisix/plugins/proxy-cache/init.lua b/apisix/plugins/proxy-cache/init.lua new file mode 100644 index 000000000000..59569980cc5c --- /dev/null +++ b/apisix/plugins/proxy-cache/init.lua @@ -0,0 +1,188 @@ +-- +-- 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 memory_handler = require("apisix.plugins.proxy-cache.memory_handler") +local disk_handler = require("apisix.plugins.proxy-cache.disk_handler") +local util = require("apisix.plugins.proxy-cache.util") +local core = require("apisix.core") +local ipairs = ipairs + +local plugin_name = "proxy-cache" + +local STRATEGY_DISK = "disk" +local STRATEGY_MEMORY = "memory" + +local schema = { + type = "object", + properties = { + cache_zone = { + type = "string", + minLength = 1, + maxLength = 100, + default = "disk_cache_one", + }, + cache_strategy = { + type = "string", + enum = {STRATEGY_DISK, STRATEGY_MEMORY}, + default = STRATEGY_DISK, + }, + cache_key = { + type = "array", + minItems = 1, + items = { + description = "a key for caching", + type = "string", + pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]], + }, + default = {"$host", "$request_uri"} + }, + cache_http_status = { + type = "array", + minItems = 1, + items = { + description = "http response status", + type = "integer", + minimum = 200, + maximum = 599, + }, + uniqueItems = true, + default = {200, 301, 404}, + }, + cache_method = { + type = "array", + minItems = 1, + items = { + description = "supported http method", + type = "string", + enum = {"GET", "POST", "HEAD"}, + }, + uniqueItems = true, + default = {"GET", "HEAD"}, + }, + hide_cache_headers = { + type = "boolean", + default = false, + }, + cache_control = { + type = "boolean", + default = false, + }, + cache_bypass = { + type = "array", + minItems = 1, + items = { + type = "string", + pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]] + }, + }, + no_cache = { + type = "array", + minItems = 1, + items = { + type = "string", + pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]] + }, + }, + cache_ttl = { + type = "integer", + minimum = 1, + default = 300, + }, + }, +} + + +local _M = { + version = 0.2, + priority = 1009, + name = plugin_name, + schema = schema, +} + + +function _M.check_schema(conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + for _, key in ipairs(conf.cache_key) do + if key == "$request_method" then + return false, "cache_key variable " .. key .. " unsupported" + end + end + + local found = false + local local_conf = core.config.local_conf() + if local_conf.apisix.proxy_cache then + for _, cache in ipairs(local_conf.apisix.proxy_cache.zones) do + if cache.name == conf.cache_zone then + found = true + end + end + + if found == false then + return false, "cache_zone " .. conf.cache_zone .. " not found" + end + end + + return true +end + + +function _M.access(conf, ctx) + core.log.info("proxy-cache plugin access phase, conf: ", core.json.delay_encode(conf)) + + local value = util.generate_complex_value(conf.cache_key, ctx) + ctx.var.upstream_cache_key = value + core.log.info("proxy-cache cache key value:", value) + + local handler + if conf.cache_strategy == STRATEGY_MEMORY then + handler = memory_handler + else + handler = disk_handler + end + + return handler.access(conf, ctx) +end + + +function _M.header_filter(conf, ctx) + core.log.info("proxy-cache plugin header filter phase, conf: ", core.json.delay_encode(conf)) + + local handler + if conf.cache_strategy == STRATEGY_MEMORY then + handler = memory_handler + else + handler = disk_handler + end + + handler.header_filter(conf, ctx) +end + + +function _M.body_filter(conf, ctx) + core.log.info("proxy-cache plugin body filter phase, conf: ", core.json.delay_encode(conf)) + + if conf.cache_strategy == STRATEGY_MEMORY then + memory_handler.body_filter(conf, ctx) + end +end + + +return _M diff --git a/apisix/plugins/proxy-cache/memory.lua b/apisix/plugins/proxy-cache/memory.lua new file mode 100644 index 000000000000..0112db63b568 --- /dev/null +++ b/apisix/plugins/proxy-cache/memory.lua @@ -0,0 +1,70 @@ +-- +-- 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 ngx = ngx +local ngx_shared = ngx.shared +local setmetatable = setmetatable +local core = require("apisix.core") + +local _M = {} +local mt = { __index = _M } + + +function _M.new(opts) + return setmetatable({ + dict = ngx_shared[opts.shdict_name], + }, mt) +end + + +function _M:set(key, obj, ttl) + local obj_json = core.json.encode(obj) + if not obj_json then + return nil, "could not encode object" + end + + local succ, err = self.dict:set(key, obj_json, ttl) + return succ and obj_json or nil, err +end + + +function _M:get(key) + -- If the key does not exist or has expired, then res_json will be nil. + local res_json, err = self.dict:get(key) + if not res_json then + if not err then + return nil, "not found" + else + return nil, err + end + end + + local res_obj, err = core.json.decode(res_json) + if not res_obj then + return nil, err + end + + return res_obj, nil +end + + +function _M:purge(key) + self.dict:delete(key) +end + + +return _M diff --git a/apisix/plugins/proxy-cache/memory_handler.lua b/apisix/plugins/proxy-cache/memory_handler.lua new file mode 100644 index 000000000000..2fd4d111ea69 --- /dev/null +++ b/apisix/plugins/proxy-cache/memory_handler.lua @@ -0,0 +1,326 @@ +-- +-- 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 memory_strategy = require("apisix.plugins.proxy-cache.memory").new +local util = require("apisix.plugins.proxy-cache.util") +local core = require("apisix.core") +local tab_new = require("table.new") +local ngx_re_gmatch = ngx.re.gmatch +local ngx_re_match = ngx.re.match +local parse_http_time = ngx.parse_http_time +local concat = table.concat +local lower = string.lower +local floor = math.floor +local tostring = tostring +local tonumber = tonumber +local ngx = ngx +local type = type +local pairs = pairs +local time = ngx.now +local max = math.max + +local CACHE_VERSION = 1 + +local _M = {} + +-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 +-- note content-length & apisix-cache-status are not strictly +-- hop-by-hop but we will be adjusting it here anyhow +local hop_by_hop_headers = { + ["connection"] = true, + ["keep-alive"] = true, + ["proxy-authenticate"] = true, + ["proxy-authorization"] = true, + ["te"] = true, + ["trailers"] = true, + ["transfer-encoding"] = true, + ["upgrade"] = true, + ["content-length"] = true, + ["apisix-cache-status"] = true, +} + + +local function include_cache_header(header) + local n_header = lower(header) + if n_header == "expires" or n_header == "cache-control" then + return true + end + + return false +end + + +local function overwritable_header(header) + local n_header = lower(header) + + return not hop_by_hop_headers[n_header] + and not ngx_re_match(n_header, "ratelimit-remaining") +end + + +-- The following format can accept: +-- Cache-Control: no-cache +-- Cache-Control: no-store +-- Cache-Control: max-age=3600 +-- Cache-Control: max-stale=3600 +-- Cache-Control: min-fresh=3600 +-- Cache-Control: private, max-age=600 +-- Cache-Control: public, max-age=31536000 +-- Refer to: https://www.holisticseo.digital/pagespeed/cache-control/ +local function parse_directive_header(h) + if not h then + return {} + end + + if type(h) == "table" then + h = concat(h, ", ") + end + + local t = {} + local res = tab_new(3, 0) + local iter = ngx_re_gmatch(h, "([^,]+)", "oj") + + local m = iter() + while m do + local _, err = ngx_re_match(m[0], [[^\s*([^=]+)(?:=(.+))?]], + "oj", nil, res) + if err then + core.log.error(err) + end + + -- store the directive token as a numeric value if it looks like a number; + -- otherwise, store the string value. for directives without token, we just + -- set the key to true + t[lower(res[1])] = tonumber(res[2]) or res[2] or true + + m = iter() + end + + return t +end + + +local function parse_resource_ttl(ctx, cc) + local max_age = cc["s-maxage"] or cc["max-age"] + + if not max_age then + local expires = ctx.var.upstream_http_expires + + -- if multiple Expires headers are present, last one wins + if type(expires) == "table" then + expires = expires[#expires] + end + + local exp_time = parse_http_time(tostring(expires)) + if exp_time then + max_age = exp_time - time() + end + end + + return max_age and max(max_age, 0) or 0 +end + + +local function cacheable_request(conf, ctx, cc) + if not util.match_method(conf, ctx) then + return false, "MISS" + end + + if conf.cache_bypass ~= nil then + local value = util.generate_complex_value(conf.cache_bypass, ctx) + core.log.info("proxy-cache cache bypass value:", value) + if value ~= nil and value ~= "" and value ~= "0" then + return false, "BYPASS" + end + end + + if conf.cache_control and (cc["no-store"] or cc["no-cache"]) then + return false, "BYPASS" + end + + return true, "" +end + + +local function cacheable_response(conf, ctx, cc) + if not util.match_status(conf, ctx) then + return false + end + + if conf.no_cache ~= nil then + local value = util.generate_complex_value(conf.no_cache, ctx) + core.log.info("proxy-cache no-cache value:", value) + + if value ~= nil and value ~= "" and value ~= "0" then + return false + end + end + + if conf.cache_control and (cc["private"] or cc["no-store"] or cc["no-cache"]) then + return false + end + + if conf.cache_control and parse_resource_ttl(ctx, cc) <= 0 then + return false + end + + return true +end + + +function _M.access(conf, ctx) + local cc = parse_directive_header(ctx.var.http_cache_control) + + if ctx.var.request_method ~= "PURGE" then + local ret, msg = cacheable_request(conf, ctx, cc) + if not ret then + core.response.set_header("Apisix-Cache-Status", msg) + return + end + end + + if not ctx.cache then + ctx.cache = { + memory = memory_strategy({shdict_name = conf.cache_zone}), + hit = false, + ttl = 0, + } + end + + local res, err = ctx.cache.memory:get(ctx.var.upstream_cache_key) + + if ctx.var.request_method == "PURGE" then + if err == "not found" then + return 404 + end + ctx.cache.memory:purge(ctx.var.upstream_cache_key) + ctx.cache = nil + return 200 + end + + if err then + core.response.set_header("Apisix-Cache-Status", "MISS") + if err ~= "not found" then + core.log.error("failed to get from cache, err: ", err) + elseif conf.cache_control and cc["only-if-cached"] then + return 504 + end + return + end + + if res.version ~= CACHE_VERSION then + core.log.warn("cache format mismatch, purging ", ctx.var.upstream_cache_key) + core.response.set_header("Apisix-Cache-Status", "BYPASS") + ctx.cache.memory:purge(ctx.var.upstream_cache_key) + return + end + + if conf.cache_control then + if cc["max-age"] and time() - res.timestamp > cc["max-age"] then + core.response.set_header("Apisix-Cache-Status", "STALE") + return + end + + if cc["max-stale"] and time() - res.timestamp - res.ttl > cc["max-stale"] then + core.response.set_header("Apisix-Cache-Status", "STALE") + return + end + + if cc["min-fresh"] and res.ttl - (time() - res.timestamp) < cc["min-fresh"] then + core.response.set_header("Apisix-Cache-Status", "STALE") + return + end + else + if time() - res.timestamp > res.ttl then + core.response.set_header("Apisix-Cache-Status", "STALE") + return + end + end + + ctx.cache.hit = true + + for key, value in pairs(res.headers) do + if conf.hide_cache_headers == true and include_cache_header(key) then + core.response.set_header(key, "") + elseif overwritable_header(key) then + core.response.set_header(key, value) + end + end + + core.response.set_header("Age", floor(time() - res.timestamp)) + core.response.set_header("Apisix-Cache-Status", "HIT") + + return res.status, res.body +end + + +function _M.header_filter(conf, ctx) + local cache = ctx.cache + if not cache or cache.hit then + return + end + + local res_headers = ngx.resp.get_headers(0, true) + + for key in pairs(res_headers) do + if conf.hide_cache_headers == true and include_cache_header(key) then + core.response.set_header(key, "") + end + end + + local cc = parse_directive_header(ctx.var.upstream_http_cache_control) + + if cacheable_response(conf, ctx, cc) then + cache.res_headers = res_headers + cache.ttl = conf.cache_control and parse_resource_ttl(ctx, cc) or conf.cache_ttl + else + ctx.cache = nil + end +end + + +function _M.body_filter(conf, ctx) + local cache = ctx.cache + if not cache or cache.hit then + return + end + + local res_body = core.response.hold_body_chunk(ctx) + if not res_body then + return + end + + local res = { + status = ngx.status, + body = res_body, + body_len = #res_body, + headers = cache.res_headers, + ttl = cache.ttl, + timestamp = time(), + version = CACHE_VERSION, + } + + local res, err = cache.memory:set(ctx.var.upstream_cache_key, res, cache.ttl) + if not res then + core.log.error("failed to set cache, err: ", err) + end + + ngx.arg[1] = res_body +end + + +return _M diff --git a/apisix/plugins/proxy-cache/util.lua b/apisix/plugins/proxy-cache/util.lua new file mode 100644 index 000000000000..f20d2fc21275 --- /dev/null +++ b/apisix/plugins/proxy-cache/util.lua @@ -0,0 +1,102 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +local core = require("apisix.core") +local ngx_re = require("ngx.re") +local tab_concat = table.concat +local string = string +local io_open = io.open +local io_close = io.close +local ngx = ngx +local ipairs = ipairs +local pairs = pairs +local tonumber = tonumber + +local _M = {} + +local tmp = {} +function _M.generate_complex_value(data, ctx) + core.table.clear(tmp) + + core.log.info("proxy-cache complex value: ", core.json.delay_encode(data)) + for i, value in ipairs(data) do + core.log.info("proxy-cache complex value index-", i, ": ", value) + + if string.byte(value, 1, 1) == string.byte('$') then + tmp[i] = ctx.var[string.sub(value, 2)] + else + tmp[i] = value + end + end + + return tab_concat(tmp, "") +end + + +-- check whether the request method match the user defined. +function _M.match_method(conf, ctx) + for _, method in ipairs(conf.cache_method) do + if method == ctx.var.request_method then + return true + end + end + + return false +end + + +-- check whether the response status match the user defined. +function _M.match_status(conf, ctx) + for _, status in ipairs(conf.cache_http_status) do + if status == ngx.status then + return true + end + end + + return false +end + + +function _M.file_exists(name) + local f = io_open(name, "r") + if f ~= nil then + io_close(f) + return true + end + return false +end + + +function _M.generate_cache_filename(cache_path, cache_levels, cache_key) + local md5sum = ngx.md5(cache_key) + local levels = ngx_re.split(cache_levels, ":") + local filename = "" + + local index = #md5sum + for k, v in pairs(levels) do + local length = tonumber(v) + index = index - length + filename = filename .. md5sum:sub(index+1, index+length) .. "/" + end + if cache_path:sub(-1) ~= "/" then + cache_path = cache_path .. "/" + end + filename = cache_path .. filename .. md5sum + return filename +end + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 1cdb76fe6111..32ed56b87c9b 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -53,19 +53,22 @@ apisix: #lua_module_hook: "my_project.my_hook" # the hook module which will be used to inject third party code into APISIX proxy_cache: # Proxy Caching configuration - cache_ttl: 10s # The default caching time if the upstream does not specify the cache time + cache_ttl: 10s # The default caching time in disk if the upstream does not specify the cache time zones: # The parameters of a cache - - name: disk_cache_one # The name of the cache, administrator can be specify - # which cache to use by name in the admin api - memory_size: 50m # The size of shared memory, it's used to store the cache index - disk_size: 1G # The size of disk, it's used to store the cache data - disk_path: /tmp/disk_cache_one # The path to store the cache data - cache_levels: 1:2 # The hierarchy levels of a cache + - name: disk_cache_one # The name of the cache, administrator can specify + # which cache to use by name in the admin api (disk|memory) + memory_size: 50m # The size of shared memory, it's used to store the cache index for + # disk strategy, store cache content for memory strategy (disk|memory) + disk_size: 1G # The size of disk, it's used to store the cache data (disk) + disk_path: /tmp/disk_cache_one # The path to store the cache data (disk) + cache_levels: 1:2 # The hierarchy levels of a cache (disk) #- name: disk_cache_two # memory_size: 50m # disk_size: 1G # disk_path: "/tmp/disk_cache_two" # cache_levels: "1:2" + - name: memory_cache + memory_size: 50m allow_admin: # http://nginx.org/en/docs/http/ngx_http_access_module.html#allow - 127.0.0.0/24 # If we don't set any IP list, then any IP access is allowed by default. diff --git a/docs/en/latest/plugins/proxy-cache.md b/docs/en/latest/plugins/proxy-cache.md index 0fc3d69d5dff..2df267156fad 100644 --- a/docs/en/latest/plugins/proxy-cache.md +++ b/docs/en/latest/plugins/proxy-cache.md @@ -32,13 +32,16 @@ The proxy-cache plugin, which provides the ability to cache upstream response da | Name | Type | Requirement | Default | Valid | Description | | ------------------ | -------------- | ----------- | ------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| cache_strategy | string | optional | disk | ["disk","memory"] | Cache strategy. Specify where the cache data is stored (memory or disk) | | cache_zone | string | optional | disk_cache_one | | Specify which cache area to use, each cache area can be configured with different paths. In addition, cache areas can be predefined in conf/config.yaml file. When the default value is not used, the specified cache area is inconsistent with the pre-defined cache area in the conf/config.yaml file, and the cache is invalid. | | cache_key | array[string] | optional | ["$host", "$request_uri"] | | key of a cache, can use variables. For example: ["$host", "$uri", "-cache-id"] | | cache_bypass | array[string] | optional | | | Whether to skip cache retrieval. That is, do not look for data in the cache. It can use variables, and note that cache data retrieval will be skipped when the value of this attribute is not empty or not '0'. For example: ["$arg_bypass"] | | cache_method | array[string] | optional | ["GET", "HEAD"] | ["GET", "POST", "HEAD"] | Decide whether to be cached according to the request method | | cache_http_status | array[integer] | optional | [200, 301, 404] | [200, 599] | Decide whether to be cached according to the upstream response status | | hide_cache_headers | boolean | optional | false | | Whether to return the Expires and Cache-Control response headers to the client, | +| cache_control | boolean | optional | false | | Whether to comply with the cache-control behavior in the HTTP specification. Only for memory strategy. | | no_cache | array[string] | optional | | | Whether to cache data, it can use variables, and note that the data will not be cached when the value of this attribute is not empty or not '0'. | +| cache_ttl | integer | optional | 300 seconds | | The default cache time. when the cache_control option is not enabled or the proxied server does not return cache header (cache-contorl or expires), it will be take effect. Only for memory strategy. | Note: diff --git a/docs/zh/latest/plugins/proxy-cache.md b/docs/zh/latest/plugins/proxy-cache.md index c9ff8cddc5df..bf3104ec9224 100644 --- a/docs/zh/latest/plugins/proxy-cache.md +++ b/docs/zh/latest/plugins/proxy-cache.md @@ -32,13 +32,16 @@ title: proxy-cache | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ------------------ | -------------- | ------ | ------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| cache_strategy | string | 可选 | disk | ["disk","memory"] | 缓存策略,指定缓存数据存储在磁盘还是内存中 | | cache_zone | string | 可选 | disk_cache_one | | 指定使用哪个缓存区域,不同的缓存区域可以配置不同的路径,在 conf/config.yaml 文件中可以预定义使用的缓存区域。当不使用默认值时,指定的缓存区域与 conf/config.yaml 文件中预定义的缓存区域不一致,缓存无效。 | | cache_key | array[string] | 可选 | ["$host", "$request_uri"] | | 缓存key,可以使用变量。例如:["$host", "$uri", "-cache-id"] | | cache_bypass | array[string] | 可选 | | | 是否跳过缓存检索,即不在缓存中查找数据,可以使用变量,需要注意当此参数的值不为空或非'0'时将会跳过缓存的检索。例如:["$arg_bypass"] | | cache_method | array[string] | 可选 | ["GET", "HEAD"] | ["GET", "POST", "HEAD"] | 根据请求method决定是否需要缓存 | | cache_http_status | array[integer] | 可选 | [200, 301, 404] | [200, 599] | 根据响应码决定是否需要缓存 | | hide_cache_headers | boolean | 可选 | false | | 是否将 Expires 和 Cache-Control 响应头返回给客户端 | +| cache_control | boolean | 可选 | false | | 是否遵守 HTTP 协议规范中的 Cache-Control 的行为 | | no_cache | array[string] | 可选 | | | 是否缓存数据,可以使用变量,需要注意当此参数的值不为空或非'0'时将不会缓存数据 | +| cache_ttl | integer | 可选 | 300 秒 | | 当选项 cache_control 未开启或开启以后服务端没有返回缓存控制头时,提供的默认缓存时间 | 注:变量以$开头,也可以使用变量和字符串的结合,但是需要以数组的形式分开写,最终变量被解析后会和字符串拼接在一起。 diff --git a/t/cli/test_main.sh b/t/cli/test_main.sh index a4bb3984ceb4..771d96846d13 100755 --- a/t/cli/test_main.sh +++ b/t/cli/test_main.sh @@ -759,7 +759,11 @@ echo ' apisix: proxy_cache: zones: - - disk_path: /tmp/disk_cache_one + - name: disk_cache_one + disk_path: /tmp/disk_cache_one + disk_size: 100m + memory_size: 20m + cache_levels: 1:2 ' > conf/config.yaml make init diff --git a/t/plugin/proxy-cache.t b/t/plugin/proxy-cache/disk.t similarity index 96% rename from t/plugin/proxy-cache.t rename to t/plugin/proxy-cache/disk.t index 1ea16ce4f732..5f759d82f18f 100644 --- a/t/plugin/proxy-cache.t +++ b/t/plugin/proxy-cache/disk.t @@ -53,6 +53,10 @@ add_block_preprocessor(sub { _EOC_ $block->set_value("http_config", $http_config); + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } }); run_tests; @@ -122,8 +126,6 @@ __DATA__ GET /t --- response_body passed ---- no_error_log -[error] @@ -166,8 +168,6 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache/ ---- no_error_log -[error] @@ -211,8 +211,6 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache/ ---- no_error_log -[error] @@ -255,8 +253,6 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache/ ---- no_error_log -[error] @@ -299,8 +295,6 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache/ ---- no_error_log -[error] @@ -344,8 +338,6 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache/ ---- no_error_log -[error] @@ -359,6 +351,7 @@ qr/failed to check the configuration of plugin proxy-cache/ [[{ "plugins": { "proxy-cache": { + "cache_key":["$host","$uri"], "cache_zone": "disk_cache_one", "cache_bypass": ["$arg_bypass"], "cache_method": ["GET"], @@ -388,8 +381,6 @@ GET /t --- error_code: 200 --- response_body passed ---- no_error_log -[error] @@ -400,8 +391,6 @@ GET /hello hello world! --- response_headers Apisix-Cache-Status: MISS ---- no_error_log -[error] @@ -414,8 +403,6 @@ hello world! Apisix-Cache-Status: HIT --- raw_response_headers_unlike Expires: ---- no_error_log -[error] @@ -426,8 +413,6 @@ GET /hello?bypass=1 hello world! --- response_headers Apisix-Cache-Status: BYPASS ---- no_error_log -[error] @@ -435,8 +420,6 @@ Apisix-Cache-Status: BYPASS --- request PURGE /hello --- error_code: 200 ---- no_error_log -[error] @@ -447,8 +430,6 @@ GET /hello?no_cache=1 hello world! --- response_headers Apisix-Cache-Status: MISS ---- no_error_log -[error] @@ -461,8 +442,6 @@ hello world! Apisix-Cache-Status: MISS --- raw_response_headers_unlike Expires: ---- no_error_log -[error] @@ -473,8 +452,6 @@ GET /hello hello world! --- response_headers Apisix-Cache-Status: HIT ---- no_error_log -[error] @@ -486,8 +463,6 @@ GET /hello-not-found qr/404 Not Found/ --- response_headers Apisix-Cache-Status: MISS ---- no_error_log -[error] @@ -499,8 +474,6 @@ GET /hello-not-found qr/404 Not Found/ --- response_headers Apisix-Cache-Status: MISS ---- no_error_log -[error] @@ -510,8 +483,6 @@ HEAD /hello-world --- error_code: 200 --- response_headers Apisix-Cache-Status: MISS ---- no_error_log -[error] @@ -521,8 +492,6 @@ HEAD /hello-world --- error_code: 200 --- response_headers Apisix-Cache-Status: MISS ---- no_error_log -[error] @@ -565,8 +534,6 @@ GET /t --- error_code: 200 --- response_body passed ---- no_error_log -[error] @@ -579,8 +546,6 @@ hello world! Apisix-Cache-Status: HIT --- response_headers_like Cache-Control: ---- no_error_log -[error] @@ -588,8 +553,6 @@ Cache-Control: --- request PURGE /hello --- error_code: 200 ---- no_error_log -[error] @@ -597,8 +560,6 @@ PURGE /hello --- request PURGE /hello-world --- error_code: 404 ---- no_error_log -[error] @@ -641,8 +602,6 @@ GET /t --- error_code: 400 --- response_body eval qr/cache_zone invalid_disk_cache not found/ ---- no_error_log -[error] @@ -686,8 +645,6 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache err: cache_key variable \$request_method unsupported/ ---- no_error_log -[error] @@ -719,8 +676,6 @@ qr/failed to check the configuration of plugin proxy-cache err: cache_key variab GET /t --- response_body passed ---- no_error_log -[error] @@ -735,8 +690,6 @@ Expires: any Apisix-Cache-Status: Foo Cache-Control: bar Expires: any ---- no_error_log -[error] @@ -779,5 +732,3 @@ GET /t --- error_code: 400 --- response_body eval qr/failed to check the configuration of plugin proxy-cache err/ ---- no_error_log -[error] diff --git a/t/plugin/proxy-cache/memory.t b/t/plugin/proxy-cache/memory.t new file mode 100644 index 000000000000..ae80b7133b62 --- /dev/null +++ b/t/plugin/proxy-cache/memory.t @@ -0,0 +1,667 @@ +# +# 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{TEST_NGINX_FORCE_RESTART_ON_TEST} = 0; +} + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); +log_level('info'); + + + +add_block_preprocessor(sub { + my ($block) = @_; + + my $http_config = $block->http_config // <<_EOC_; + + # for proxy cache + proxy_cache_path /tmp/disk_cache_one levels=1:2 keys_zone=disk_cache_one:50m inactive=1d max_size=1G; + proxy_cache_path /tmp/disk_cache_two levels=1:2 keys_zone=disk_cache_two:50m inactive=1d max_size=1G; + lua_shared_dict memory_cache 50m; + + # for proxy cache + map \$upstream_cache_zone \$upstream_cache_zone_info { + disk_cache_one /tmp/disk_cache_one,1:2; + disk_cache_two /tmp/disk_cache_two,1:2; + } + + server { + listen 1986; + server_tokens off; + + location / { + expires 60s; + + if (\$arg_expires) { + expires \$arg_expires; + } + + if (\$arg_cc) { + expires off; + add_header Cache-Control \$arg_cc; + } + + return 200 "hello world!"; + } + + location /hello-not-found { + return 404; + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity check (invalid cache strategy) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "proxy-cache": { + "cache_strategy": "network", + "cache_key":["$host","$uri"], + "cache_zone": "disk_cache_one", + "cache_bypass": ["$arg_bypass"], + "cache_method": ["GET"], + "cache_http_status": [200], + "hide_cache_headers": true, + "no_cache": ["$arg_no_cache"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1986": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/failed to check the configuration of plugin proxy-cache err: property \\"cache_strategy\\" validation failed: matches none of the enum values/ + + + +=== TEST 2: sanity check (invalid cache_zone when specifying cache_strategy as memory) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "proxy-cache": { + "cache_strategy": "memory", + "cache_key":["$host","$uri"], + "cache_zone": "invalid_cache_zone", + "cache_bypass": ["$arg_bypass"], + "cache_method": ["GET"], + "cache_http_status": [200], + "hide_cache_headers": true, + "no_cache": ["$arg_no_cache"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1986": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/failed to check the configuration of plugin proxy-cache err: cache_zone invalid_cache_zone not found"/ + + + +=== TEST 3: sanity check (normal case for memory strategy) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "proxy-cache": { + "cache_strategy": "memory", + "cache_key":["$host","$uri"], + "cache_zone": "memory_cache", + "cache_bypass": ["$arg_bypass"], + "cache_method": ["GET"], + "hide_cache_headers": false, + "cache_ttl": 300, + "cache_http_status": [200], + "no_cache": ["$arg_no_cache"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1986": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +passed + + + +=== TEST 4: hit route (cache miss) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 5: hit route (cache hit) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: HIT + + + +=== TEST 6: hit route (cache bypass) +--- request +GET /hello?bypass=1 +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: BYPASS + + + +=== TEST 7: purge cache +--- request +PURGE /hello +--- error_code: 200 + + + +=== TEST 8: hit route (nocache) +--- request +GET /hello?no_cache=1 +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 9: hit route (there's no cache indeed) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS +--- raw_response_headers_unlike +Expires: + + + +=== TEST 10: hit route (will be cached) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: HIT + + + +=== TEST 11: hit route (not found) +--- request +GET /hello-not-found +--- error_code: 404 +--- response_body eval +qr/404 Not Found/ +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 12: hit route (404 there's no cache indeed) +--- request +GET /hello-not-found +--- error_code: 404 +--- response_body eval +qr/404 Not Found/ +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 13: hit route (HEAD method) +--- request +HEAD /hello-world +--- error_code: 200 +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 14: hit route (HEAD method there's no cache) +--- request +HEAD /hello-world +--- error_code: 200 +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 15: purge cache +--- request +PURGE /hello +--- error_code: 200 + + + +=== TEST 16: purge cache (not found) +--- request +PURGE /hello-world +--- error_code: 404 + + + +=== TEST 17: hide cache headers = false +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "proxy-cache": { + "cache_strategy": "memory", + "cache_key":["$host","$uri"], + "cache_zone": "memory_cache", + "cache_bypass": ["$arg_bypass"], + "cache_method": ["GET"], + "cache_ttl": 300, + "cache_http_status": [200], + "hide_cache_headers": false, + "no_cache": ["$arg_no_cache"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1986": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +passed + + + +=== TEST 18: hit route (catch the cache headers) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS +--- response_headers_like +Cache-Control: + + + +=== TEST 19: don't override cache relative headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/echo" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 20: hit route +--- request +GET /echo +--- more_headers +Apisix-Cache-Status: Foo +Cache-Control: bar +Expires: any +--- response_headers +Apisix-Cache-Status: Foo +Cache-Control: bar +Expires: any + + + +=== TEST 21: set cache_ttl to 1 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "proxy-cache": { + "cache_strategy": "memory", + "cache_key":["$host","$uri"], + "cache_zone": "memory_cache", + "cache_bypass": ["$arg_bypass"], + "cache_method": ["GET"], + "cache_ttl": 2, + "cache_http_status": [200], + "hide_cache_headers": false, + "no_cache": ["$arg_no_cache"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1986": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +passed + + + +=== TEST 22: hit route (MISS) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 23: hit route (HIT) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: HIT +--- wait: 2 + + + +=== TEST 24: hit route (MISS) +--- request +GET /hello +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 25: enable cache_control option +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "proxy-cache": { + "cache_strategy": "memory", + "cache_key":["$host","$uri"], + "cache_zone": "memory_cache", + "cache_bypass": ["$arg_bypass"], + "cache_control": true, + "cache_method": ["GET"], + "cache_ttl": 10, + "cache_http_status": [200], + "hide_cache_headers": false, + "no_cache": ["$arg_no_cache"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1986": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello*" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- error_code: 200 +--- response_body +passed + + + +=== TEST 26: hit route (MISS) +--- request +GET /hello +--- more_headers +Cache-Control: max-age=60 +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS +--- wait: 1 + + + +=== TEST 27: hit route (request header cache-control with max-age) +--- request +GET /hello +--- more_headers +Cache-Control: max-age=1 +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: STALE + + + +=== TEST 28: hit route (request header cache-control with min-fresh) +--- request +GET /hello +--- more_headers +Cache-Control: min-fresh=300 +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: STALE +--- wait: 1 + + + +=== TEST 29: purge cache +--- request +PURGE /hello +--- error_code: 200 + + + +=== TEST 30: hit route (request header cache-control with no-store) +--- request +GET /hello +--- more_headers +Cache-Control: no-store +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: BYPASS + + + +=== TEST 31: hit route (request header cache-control with no-cache) +--- request +GET /hello +--- more_headers +Cache-Control: no-cache +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: BYPASS + + + +=== TEST 32: hit route (response header cache-control with private) +--- request +GET /hello?cc=private +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 33: hit route (response header cache-control with no-store) +--- request +GET /hello?cc=no-store +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 34: hit route (response header cache-control with no-cache) +--- request +GET /hello?cc=no-cache +--- response_body chop +hello world! +--- response_headers +Apisix-Cache-Status: MISS + + + +=== TEST 35: hit route (request header cache-control with only-if-cached) +--- request +GET /hello +--- more_headers +Cache-Control: only-if-cached +--- error_code: 504 From 2f250d53698b0fa617ea981d2d488ebd756bb655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 22 Oct 2021 08:49:58 +0800 Subject: [PATCH 022/260] feat(limit-req): support multiple variables as key (#5302) --- apisix/plugins/limit-req.lua | 33 +++++-- docs/en/latest/plugins/limit-req.md | 3 +- docs/zh/latest/plugins/limit-req.md | 5 +- t/admin/plugins.t | 2 +- t/plugin/limit-req.t | 5 +- t/plugin/limit-req2.t | 137 ++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 16 deletions(-) diff --git a/apisix/plugins/limit-req.lua b/apisix/plugins/limit-req.lua index bd7ef4a1c6cd..6768dc1f35a4 100644 --- a/apisix/plugins/limit-req.lua +++ b/apisix/plugins/limit-req.lua @@ -29,9 +29,10 @@ local schema = { properties = { rate = {type = "number", exclusiveMinimum = 0}, burst = {type = "number", minimum = 0}, - key = {type = "string", - enum = {"remote_addr", "server_addr", "http_x_real_ip", - "http_x_forwarded_for", "consumer_name"}, + key = {type = "string"}, + key_type = {type = "string", + enum = {"var", "var_combination"}, + default = "var", }, rejected_code = { type = "integer", minimum = 200, maximum = 599, default = 503 @@ -83,17 +84,31 @@ function _M.access(conf, ctx) return 500 end + local conf_key = conf.key local key - if conf.key == "consumer_name" then - if not ctx.consumer_name then - core.log.error("consumer not found.") - return 500, { message = "Consumer not found."} + if conf.key_type == "var_combination" then + local err, n_resolved + key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var); + if err then + core.log.error("could not resolve vars in ", conf_key, " error: ", err) + end + + if n_resolved == 0 then + key = nil end - key = ctx.consumer_name .. ctx.conf_type .. ctx.conf_version else - key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version + key = ctx.var[conf_key] end + + if key == nil then + core.log.info("bypass the limit req as the key is empty") + -- Bypass the limit req when the key is empty. + -- This behavior is the same as Nginx + return + end + + key = key .. ctx.conf_type .. ctx.conf_version core.log.info("limit key: ", key) local delay, err = lim:incoming(key, true) diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index 1792adb7c436..166fa1bf9710 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -40,7 +40,8 @@ limit request rate using the "leaky bucket" method. | ------------- | ------- | ----------- | ------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | rate | integer | required | | rate > 0 | the specified request rate (number per second) threshold. Requests exceeding this rate (and below `burst`) will get delayed to conform to the rate. | | burst | integer | required | | burst >= 0 | the number of excessive requests per second allowed to be delayed. Requests exceeding this hard limit will get rejected immediately. | -| key | string | required | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | the user specified key to limit the rate, now accept those as key: "remote_addr"(client's IP), "server_addr"(server's IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer's username). | +| key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | +| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr|$consumer_name". | | rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected. | | rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. | | nodelay | boolean | optional | false | | If nodelay flag is true, bursted requests will not get delayed | diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index 7b57c151696a..fb7b7d5af8e3 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -39,8 +39,9 @@ title: limit-req | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ------------- | ------- | ------ | ------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | rate | integer | 必须 | | rate > 0 | 指定的请求速率(以秒为单位),请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求会被加上延时。 | -| burst | integer | 必须 | | burst >= 0 | t请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 | -| key | string | 必须 | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | 用来做请求计数的依据,当前接受的 key 有:"remote_addr"(客户端IP地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP","consumer_name"(consumer 的 username)。 | +| burst | integer | 必须 | | burst >= 0 | 请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 | +| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | +| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr|$consumer_name"。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码。 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | | nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求不会加上延迟, 如果是 false,则会加上延迟。 | diff --git a/t/admin/plugins.t b/t/admin/plugins.t index b0d5d8f82c23..b5196dd0a042 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -66,7 +66,7 @@ GET /apisix/admin/plugins ngx.HTTP_GET, nil, [[ -{"properties":{"rate":{"exclusiveMinimum":0,"type":"number"},"burst":{"minimum":0,"type":"number"},"key":{"enum":["remote_addr","server_addr","http_x_real_ip","http_x_forwarded_for","consumer_name"],"type":"string"},"rejected_code":{"type":"integer","default":503,"minimum":200,"maximum":599}},"required":["rate","burst","key"],"type":"object"} + {"type":"object","required":["rate","burst","key"],"properties":{"rate":{"type":"number","exclusiveMinimum":0},"key_type":{"type":"string","enum":["var","var_combination"],"default":"var"},"burst":{"type":"number","minimum":0},"disable":{"type":"boolean"},"nodelay":{"type":"boolean","default":false},"key":{"type":"string"},"rejected_code":{"type":"integer","minimum":200,"maximum":599,"default":503},"rejected_msg":{"type":"string","minLength":1},"allow_degradation":{"type":"boolean","default":false}}} ]] ) diff --git a/t/plugin/limit-req.t b/t/plugin/limit-req.t index dd1a355dd4dd..d3d03e30fa6f 100644 --- a/t/plugin/limit-req.t +++ b/t/plugin/limit-req.t @@ -693,11 +693,10 @@ passed === TEST 18: get "consumer_name" is empty --- request GET /hello ---- error_code: 500 --- response_body -{"message":"Consumer not found."} +hello world --- error_log -[error] +bypass the limit req as the key is empty diff --git a/t/plugin/limit-req2.t b/t/plugin/limit-req2.t index 9ed83e763c31..dc3e4ce9dcea 100644 --- a/t/plugin/limit-req2.t +++ b/t/plugin/limit-req2.t @@ -119,3 +119,140 @@ passed ["GET /hello", "GET /hello"] --- error_code eval [200, 200] + + + +=== TEST 5: key type is var_combination +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-req": { + "rate": 0.1, + "burst": 0.1, + "rejected_code": 503, + "key": "$http_a $http_b", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 6: exceed the burst +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {a = 1}}) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,503] + + + +=== TEST 7: don't exceed the burst +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {a = i}}) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200] + + + +=== TEST 8: bypass empty key +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200] +--- error_log +bypass the limit req as the key is empty From 6aaf1f87554f2c39a0395e4187343be3cd225ea1 Mon Sep 17 00:00:00 2001 From: "Yu.Bozhong" Date: Fri, 22 Oct 2021 10:10:03 +0800 Subject: [PATCH 023/260] docs: add simplified Chinese translation for plugin authz-casbin (#5274) --- docs/zh/latest/config.json | 3 +- docs/zh/latest/plugins/authz-casbin.md | 249 +++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 docs/zh/latest/plugins/authz-casbin.md diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 8e895a494e94..5e8f25ad1003 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -63,7 +63,8 @@ "plugins/jwt-auth", "plugins/basic-auth", "plugins/openid-connect", - "plugins/hmac-auth" + "plugins/hmac-auth", + "plugins/authz-casbin" ] }, { diff --git a/docs/zh/latest/plugins/authz-casbin.md b/docs/zh/latest/plugins/authz-casbin.md new file mode 100644 index 000000000000..8e0b0d88701b --- /dev/null +++ b/docs/zh/latest/plugins/authz-casbin.md @@ -0,0 +1,249 @@ +--- +title: authz-casbin +--- + + + +## 目录 + +- [**简介**](#简介) +- [**属性**](#属性) +- [**元数据**](#元数据) +- [**如何启用**](#如何启用) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) +- [**示例**](#示例) + +## 简介 + +`authz-casbin` 是一个基于 [Lua Casbin](https://github.com/casbin/lua-casbin/) 的访问控制插件,该插件支持基于各种访问控制模型的授权场景。 + +有关如何创建鉴权模型和鉴权策略的详细文档, 请参阅 [Casbin](https://casbin.org/docs/en/supported-models)。 + +## 属性 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ----------- | ------ | ------ | ------- | ----- | --------------------------- | +| model_path | string | 必须 | | | Casbin 鉴权模型配置文件路径 | +| policy_path | string | 必须 | | | Casbin 鉴权策略配置文件路径 | +| model | string | 必须 | | | Casbin 鉴权模型的文本定义 | +| policy | string | 必须 | | | Casbin 鉴权策略的文本定义 | +| username | string | 必须 | | | 描述请求中有可以通过访问控制的用户名 | + +**注意**: 在插件配置中指定 `model_path`、`policy_path` 和 `username`,或者在插件配置中指定 `model`、`policy` 和 `username` 来使插件生效。如果想要使所有的路由共享 Casbin 配置,可以先在插件元数据中指定鉴权模型和鉴权策略,然后在指定路由的插件配置中指定 `username`。 + +## 元数据 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ----------- | ------ | ------ | ----- | ----- | ------------------ | +| model | string | 必须 | | | Casbin 鉴权模型的文本定义 | +| policy | string | 必须 | | | Casbin 鉴权策略的文本定义 | + +## 如何启用 + +该插件可以通过在任意路由上配置 `鉴权模型/鉴权策略文件路径` 或 `鉴权模型/鉴权策略文本` 来启用。 + +### 通过配置文件启用 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "authz-casbin": { + "model_path": "/path/to/model.conf", + "policy_path": "/path/to/policy.csv", + "username": "user" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" +}' +``` + +上述请求会根据鉴权模型/鉴权策略文件中的定义创建一个 Casbin enforcer。 + +### 通过路由配置启用 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "authz-casbin": { + "model": "[request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)", + + "policy": "p, *, /, GET + p, admin, *, * + g, alice, admin", + + "username": "user" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" +}' +``` + +上述请求会根据鉴权模型和鉴权策略的定义创建一个 Casbin enforcer。 + +### 通过 plugin metadata 配置模型/策略 + +首先,使用 Admin API 发送一个 `PUT` 请求,将鉴权模型和鉴权策略的配置信息添加到插件的元数据中。所有通过这种方式创建的路由都会带有一个带插件元数据配置的 Casbin enforcer。同时也可以使用 `PUT` 请求更新鉴权模型和鉴权策略配置信息,该插件将会自动同步最新的配置信息。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/authz-casbin -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -X PUT -d ' +{ +"model": "[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act)", + +"policy": "p, *, /, GET +p, admin, *, * +g, alice, admin" +}' +``` + +通过发送以下请求可以将该插件添加到路由上。注意,此处只需要配置 `username`,不需要再增加鉴权模型/鉴权策略的定义。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "authz-casbin": { + "username": "user" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/*" +}' +``` + +**注意**: 插件路由配置比插件元数据配置有更高的优先级。因此,如果插件路由配置中存在鉴权模型/鉴权策略配置,插件将优先使用插件路由的配置而不是插件元数据中的配置。 + +## 测试插件 + +首先定义测试鉴权模型: + +```conf +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = (g(r.sub, p.sub) || keyMatch(r.sub, p.sub)) && keyMatch(r.obj, p.obj) && keyMatch(r.act, p.act) +``` + +然后添加测试鉴权策略: + +```conf +p, *, /, GET +p, admin, *, * +g, alice, admin +``` + +以上授权策略规定了任何人都可以使用 `GET` 请求方法访问主页(`/`),而只有具有管理权限的用户可以访问其他页面和使用其他请求方法。 + +例如,在这里,任何人都可以用 `GET` 请求方法访问主页,返回正常。 + +```shell +curl -i http://127.0.0.1:9080/ -X GET +``` + +未经授权的用户如 `bob` 访问除 `/` 以外的任何其他页面将得到一个 403 错误: + +```shell +curl -i http://127.0.0.1:9080/res -H 'user: bob' -X GET +HTTP/1.1 403 Forbidden +``` + +拥有管理权限的人 `alice` 则可以访问其它页面。 + +```shell +curl -i http://127.0.0.1:9080/res -H 'user: alice' -X GET +``` + +## 禁用插件 + +在插件配置中删除相应的 json 配置,以禁用 `authz-casbin` 插件。由于 Apache APISIX 插件是热加载的,因此不需要重新启动 Apache APISIX。 + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/*", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +## 示例 + +更多鉴权模型和鉴权策略使用的例子请参考 [Casbin 示例](https://github.com/casbin/lua-casbin/tree/master/examples)。 From a6f757e08f7433b996d7725be0a90de55169336d Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 22 Oct 2021 14:15:33 +0800 Subject: [PATCH 024/260] refactor: unify code style in Makefile (#5248) --- .github/workflows/build.yml | 5 +- .github/workflows/centos7-ci.yml | 2 +- Makefile | 243 ++++++++++----------- ci/linux_apisix_current_luarocks_runner.sh | 2 +- docs/en/latest/FAQ.md | 2 +- docs/zh/latest/FAQ.md | 2 +- rockspec/apisix-master-0.rockspec | 10 +- 7 files changed, 132 insertions(+), 134 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8fc220037fe..3597b40028f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,13 +56,12 @@ jobs: - name: Linux launch common services run: | - project_compose_ci=ci/pod/docker-compose.common.yml make ci-env-up + make ci-env-up project_compose_ci=ci/pod/docker-compose.common.yml - name: Create tarball if: ${{ startsWith(github.ref, 'refs/heads/release/') }} run: | - export VERSION=${{ steps.branch_env.outputs.version }} - make compress-tar + make compress-tar project_version=${{ steps.branch_env.outputs.version }} - name: Remove source code if: ${{ startsWith(github.ref, 'refs/heads/release/') }} diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index cffea25f8908..c3cd3a21afde 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -34,7 +34,7 @@ jobs: - name: Linux launch common services run: | - project_compose_ci=ci/pod/docker-compose.common.yml make ci-env-up + make ci-env-up project_compose_ci=ci/pod/docker-compose.common.yml - name: Build rpm package if: ${{ startsWith(github.ref, 'refs/heads/release/') }} diff --git a/Makefile b/Makefile index 002ed2e8a6b2..c993ffe41a0d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ SHELL := /bin/bash -o pipefail # Project basic setting project_name ?= apache-apisix -project_version ?= latest +project_version ?= master project_compose_ci ?= ci/pod/docker-compose.yml project_release_name ?= $(project_name)-$(project_version)-src @@ -33,55 +33,55 @@ ENV_OS_NAME ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') ENV_OS_ARCH ?= $(shell uname -m | tr '[:upper:]' '[:lower:]') ENV_APISIX ?= $(CURDIR)/bin/apisix ENV_GIT ?= git +ENV_TAR ?= tar +ENV_INSTALL ?= install ENV_DOCKER ?= docker ENV_DOCKER_COMPOSE ?= docker-compose --project-directory $(CURDIR) -p $(project_name) -f $(project_compose_ci) -ENV_NGINX ?= nginx -p $(CURDIR) -c $(CURDIR)/conf/nginx.conf - - -# OSX archive `._` cache file -ifeq ($(ENV_OS_NAME), darwin) - ENV_TAR ?= COPYFILE_DISABLE=1 tar -else - ENV_TAR ?= tar +ENV_NGINX ?= $(ENV_NGINX_EXEC) -p $(CURDIR) -c $(CURDIR)/conf/nginx.conf +ENV_NGINX_EXEC := $(shell which openresty 2>/dev/null || which nginx 2>/dev/null) +ENV_OPENSSL_PREFIX ?= $(addprefix $(ENV_NGINX_PREFIX), openssl) +ENV_LUAROCKS ?= luarocks +## These variables can be injected by luarocks +ENV_INST_PREFIX ?= /usr +ENV_INST_LUADIR ?= $(ENV_INST_PREFIX)/share/lua/5.1 +ENV_INST_BINDIR ?= $(ENV_INST_PREFIX)/bin +ENV_HOMEBREW_PREFIX ?= /usr/local + +ifneq ($(shell whoami), root) + ENV_LUAROCKS_FLAG_LOCAL := --local endif +ifdef ENV_LUAROCKS_SERVER + ENV_LUAROCKS_SERVER_OPT := --server $(ENV_LUAROCKS_SERVER) +endif -# OLD VAR -INST_PREFIX ?= /usr -INST_LIBDIR ?= $(INST_PREFIX)/lib64/lua/5.1 -INST_LUADIR ?= $(INST_PREFIX)/share/lua/5.1 -INST_BINDIR ?= /usr/bin -INSTALL ?= install -OR_EXEC ?= $(shell which openresty || which nginx) -LUAROCKS ?= luarocks -LUAROCKS_VER ?= $(shell luarocks --version | grep -E -o "luarocks [0-9]+.") -OR_PREFIX ?= $(shell $(OR_EXEC) -V 2>&1 | grep -Eo 'prefix=(.*)/nginx\s+' | grep -Eo '/.*/') -OPENSSL_PREFIX ?= $(addprefix $(OR_PREFIX), openssl) -HOMEBREW_PREFIX ?= /usr/local - -# OpenResty 1.17.8 or higher version uses openssl111 as the openssl dirname. -ifeq ($(shell test -d $(addprefix $(OR_PREFIX), openssl111) && echo -n yes), yes) - OPENSSL_PREFIX=$(addprefix $(OR_PREFIX), openssl111) +# Execute only in the presence of ENV_NGINX_EXEC to avoid unexpected error output +ifneq ($(ENV_NGINX_EXEC), ) + ENV_NGINX_PREFIX := $(shell $(ENV_NGINX_EXEC) -V 2>&1 | grep -Eo 'prefix=(.*)/nginx\s+' | grep -Eo '/.*/') + # OpenResty 1.17.8 or higher version uses openssl111 as the openssl dirname. + ifeq ($(shell test -d $(addprefix $(ENV_NGINX_PREFIX), openssl111) && echo -n yes), yes) + ENV_OPENSSL_PREFIX := $(addprefix $(ENV_NGINX_PREFIX), openssl111) + endif endif +# ENV patch for darwin ifeq ($(ENV_OS_NAME), darwin) ifeq ($(ENV_OS_ARCH), arm64) - HOMEBREW_PREFIX=/opt/homebrew + ENV_HOMEBREW_PREFIX := /opt/homebrew endif - LUAROCKS=luarocks --lua-dir=$(HOMEBREW_PREFIX)/opt/lua@5.1 - ifeq ($(shell test -d $(HOMEBREW_PREFIX)/opt/openresty-openssl && echo yes), yes) - OPENSSL_PREFIX=$(HOMEBREW_PREFIX)/opt/openresty-openssl + + # OSX archive `._` cache file + ENV_TAR := COPYFILE_DISABLE=1 $(ENV_TAR) + ENV_LUAROCKS := $(ENV_LUAROCKS) --lua-dir=$(ENV_HOMEBREW_PREFIX)/opt/lua@5.1 + + ifeq ($(shell test -d $(ENV_HOMEBREW_PREFIX)/opt/openresty-openssl && echo -n yes), yes) + ENV_OPENSSL_PREFIX := $(ENV_HOMEBREW_PREFIX)/opt/openresty-openssl endif - ifeq ($(shell test -d $(HOMEBREW_PREFIX)/opt/openresty-openssl111 && echo yes), yes) - OPENSSL_PREFIX=$(HOMEBREW_PREFIX)/opt/openresty-openssl111 + ifeq ($(shell test -d $(ENV_HOMEBREW_PREFIX)/opt/openresty-openssl111 && echo -n yes), yes) + ENV_OPENSSL_PREFIX := $(ENV_HOMEBREW_PREFIX)/opt/openresty-openssl111 endif endif -LUAROCKS_SERVER_OPT = -ifneq ($(LUAROCKS_SERVER), ) - LUAROCKS_SERVER_OPT = --server ${LUAROCKS_SERVER} -endif - # Makefile basic extension function _color_red =\E[1;31m @@ -119,12 +119,13 @@ endef # Makefile target .PHONY: runtime runtime: -ifeq ($(OR_EXEC), ) +ifeq ($(ENV_NGINX_EXEC), ) ifeq ("$(wildcard /usr/local/openresty-debug/bin/openresty)", "") - @echo "WARNING: OpenResty not found. You have to install OpenResty and add the binary file to PATH before install Apache APISIX." + @$(call func_echo_warn_status, "WARNING: OpenResty not found. You have to install OpenResty and add the binary file to PATH before install Apache APISIX.") exit 1 else - OR_EXEC=/usr/local/openresty-debug/bin/openresty + $(eval ENV_NGINX_EXEC := /usr/local/openresty-debug/bin/openresty) + @$(call func_echo_status, "Use openresty-debug as default runtime") endif endif @@ -147,24 +148,20 @@ help: ### deps : Installation dependencies .PHONY: deps deps: runtime -ifeq ($(LUAROCKS_VER),luarocks 3.) - mkdir -p ~/.luarocks -ifeq ($(shell whoami),root) - $(LUAROCKS) config variables.OPENSSL_LIBDIR $(addprefix $(OPENSSL_PREFIX), /lib) - $(LUAROCKS) config variables.OPENSSL_INCDIR $(addprefix $(OPENSSL_PREFIX), /include) -else - $(LUAROCKS) config --local variables.OPENSSL_LIBDIR $(addprefix $(OPENSSL_PREFIX), /lib) - $(LUAROCKS) config --local variables.OPENSSL_INCDIR $(addprefix $(OPENSSL_PREFIX), /include) -endif - $(LUAROCKS) install rockspec/apisix-master-0.rockspec --tree=deps --only-deps --local $(LUAROCKS_SERVER_OPT) -else - @echo "WARN: You're not using LuaRocks 3.x, please add the following items to your LuaRocks config file:" - @echo "variables = {" - @echo " OPENSSL_LIBDIR=$(addprefix $(OPENSSL_PREFIX), /lib)" - @echo " OPENSSL_INCDIR=$(addprefix $(OPENSSL_PREFIX), /include)" - @echo "}" - $(LUAROCKS) install rockspec/apisix-master-0.rockspec --tree=deps --only-deps --local $(LUAROCKS_SERVER_OPT) -endif + $(eval ENV_LUAROCKS_VER := $(shell $(ENV_LUAROCKS) --version | grep -E -o "luarocks [0-9]+.")) + @if [ '$(ENV_LUAROCKS_VER)' = 'luarocks 3.' ]; then \ + mkdir -p ~/.luarocks; \ + $(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_LIBDIR $(addprefix $(ENV_OPENSSL_PREFIX), /lib); \ + $(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_INCDIR $(addprefix $(ENV_OPENSSL_PREFIX), /include); \ + $(ENV_LUAROCKS) install rockspec/apisix-master-0.rockspec --tree=deps --only-deps --local $(ENV_LUAROCKS_SERVER_OPT); \ + else \ + $(call func_echo_warn_status, "WARNING: You're not using LuaRocks 3.x; please add the following items to your LuaRocks config file:"); \ + echo "variables = {"; \ + echo " OPENSSL_LIBDIR=$(addprefix $(ENV_OPENSSL_PREFIX), /lib)"; \ + echo " OPENSSL_INCDIR=$(addprefix $(ENV_OPENSSL_PREFIX), /include)"; \ + echo "}"; \ + $(ENV_LUAROCKS) install rockspec/apisix-master-0.rockspec --tree=deps --only-deps --local $(ENV_LUAROCKS_SERVER_OPT); \ + fi ### utils : Installation tools @@ -249,96 +246,96 @@ reload: runtime ### install : Install the apisix (only for luarocks) .PHONY: install install: runtime - $(INSTALL) -d /usr/local/apisix/ - $(INSTALL) -d /usr/local/apisix/logs/ - $(INSTALL) -d /usr/local/apisix/conf/cert - $(INSTALL) conf/mime.types /usr/local/apisix/conf/mime.types - $(INSTALL) conf/config.yaml /usr/local/apisix/conf/config.yaml - $(INSTALL) conf/config-default.yaml /usr/local/apisix/conf/config-default.yaml - $(INSTALL) conf/debug.yaml /usr/local/apisix/conf/debug.yaml - $(INSTALL) conf/cert/* /usr/local/apisix/conf/cert/ + $(ENV_INSTALL) -d /usr/local/apisix/ + $(ENV_INSTALL) -d /usr/local/apisix/logs/ + $(ENV_INSTALL) -d /usr/local/apisix/conf/cert + $(ENV_INSTALL) conf/mime.types /usr/local/apisix/conf/mime.types + $(ENV_INSTALL) conf/config.yaml /usr/local/apisix/conf/config.yaml + $(ENV_INSTALL) conf/config-default.yaml /usr/local/apisix/conf/config-default.yaml + $(ENV_INSTALL) conf/debug.yaml /usr/local/apisix/conf/debug.yaml + $(ENV_INSTALL) conf/cert/* /usr/local/apisix/conf/cert/ - $(INSTALL) -d $(INST_LUADIR)/apisix - $(INSTALL) apisix/*.lua $(INST_LUADIR)/apisix/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix + $(ENV_INSTALL) apisix/*.lua $(ENV_INST_LUADIR)/apisix/ - $(INSTALL) -d $(INST_LUADIR)/apisix/admin - $(INSTALL) apisix/admin/*.lua $(INST_LUADIR)/apisix/admin/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/admin + $(ENV_INSTALL) apisix/admin/*.lua $(ENV_INST_LUADIR)/apisix/admin/ - $(INSTALL) -d $(INST_LUADIR)/apisix/balancer - $(INSTALL) apisix/balancer/*.lua $(INST_LUADIR)/apisix/balancer/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/balancer + $(ENV_INSTALL) apisix/balancer/*.lua $(ENV_INST_LUADIR)/apisix/balancer/ - $(INSTALL) -d $(INST_LUADIR)/apisix/control - $(INSTALL) apisix/control/*.lua $(INST_LUADIR)/apisix/control/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/control + $(ENV_INSTALL) apisix/control/*.lua $(ENV_INST_LUADIR)/apisix/control/ - $(INSTALL) -d $(INST_LUADIR)/apisix/core - $(INSTALL) apisix/core/*.lua $(INST_LUADIR)/apisix/core/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/core + $(ENV_INSTALL) apisix/core/*.lua $(ENV_INST_LUADIR)/apisix/core/ - $(INSTALL) -d $(INST_LUADIR)/apisix/core/dns - $(INSTALL) apisix/core/dns/*.lua $(INST_LUADIR)/apisix/core/dns + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/core/dns + $(ENV_INSTALL) apisix/core/dns/*.lua $(ENV_INST_LUADIR)/apisix/core/dns - $(INSTALL) -d $(INST_LUADIR)/apisix/cli - $(INSTALL) apisix/cli/*.lua $(INST_LUADIR)/apisix/cli/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/cli + $(ENV_INSTALL) apisix/cli/*.lua $(ENV_INST_LUADIR)/apisix/cli/ - $(INSTALL) -d $(INST_LUADIR)/apisix/discovery - $(INSTALL) apisix/discovery/*.lua $(INST_LUADIR)/apisix/discovery/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/discovery + $(ENV_INSTALL) apisix/discovery/*.lua $(ENV_INST_LUADIR)/apisix/discovery/ - $(INSTALL) -d $(INST_LUADIR)/apisix/http - $(INSTALL) apisix/http/*.lua $(INST_LUADIR)/apisix/http/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/http + $(ENV_INSTALL) apisix/http/*.lua $(ENV_INST_LUADIR)/apisix/http/ - $(INSTALL) -d $(INST_LUADIR)/apisix/http/router - $(INSTALL) apisix/http/router/*.lua $(INST_LUADIR)/apisix/http/router/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/http/router + $(ENV_INSTALL) apisix/http/router/*.lua $(ENV_INST_LUADIR)/apisix/http/router/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins - $(INSTALL) apisix/plugins/*.lua $(INST_LUADIR)/apisix/plugins/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins + $(ENV_INSTALL) apisix/plugins/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/ext-plugin - $(INSTALL) apisix/plugins/ext-plugin/*.lua $(INST_LUADIR)/apisix/plugins/ext-plugin/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ext-plugin + $(ENV_INSTALL) apisix/plugins/ext-plugin/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ext-plugin/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/grpc-transcode - $(INSTALL) apisix/plugins/grpc-transcode/*.lua $(INST_LUADIR)/apisix/plugins/grpc-transcode/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/grpc-transcode + $(ENV_INSTALL) apisix/plugins/grpc-transcode/*.lua $(ENV_INST_LUADIR)/apisix/plugins/grpc-transcode/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/ip-restriction - $(INSTALL) apisix/plugins/ip-restriction/*.lua $(INST_LUADIR)/apisix/plugins/ip-restriction/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/ip-restriction + $(ENV_INSTALL) apisix/plugins/ip-restriction/*.lua $(ENV_INST_LUADIR)/apisix/plugins/ip-restriction/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/limit-conn - $(INSTALL) apisix/plugins/limit-conn/*.lua $(INST_LUADIR)/apisix/plugins/limit-conn/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/limit-conn + $(ENV_INSTALL) apisix/plugins/limit-conn/*.lua $(ENV_INST_LUADIR)/apisix/plugins/limit-conn/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/limit-count - $(INSTALL) apisix/plugins/limit-count/*.lua $(INST_LUADIR)/apisix/plugins/limit-count/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/limit-count + $(ENV_INSTALL) apisix/plugins/limit-count/*.lua $(ENV_INST_LUADIR)/apisix/plugins/limit-count/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/prometheus - $(INSTALL) apisix/plugins/prometheus/*.lua $(INST_LUADIR)/apisix/plugins/prometheus/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/prometheus + $(ENV_INSTALL) apisix/plugins/prometheus/*.lua $(ENV_INST_LUADIR)/apisix/plugins/prometheus/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/proxy-cache - $(INSTALL) apisix/plugins/proxy-cache/*.lua $(INST_LUADIR)/apisix/plugins/proxy-cache/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/proxy-cache + $(ENV_INSTALL) apisix/plugins/proxy-cache/*.lua $(ENV_INST_LUADIR)/apisix/plugins/proxy-cache/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/serverless - $(INSTALL) apisix/plugins/serverless/*.lua $(INST_LUADIR)/apisix/plugins/serverless/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/serverless + $(ENV_INSTALL) apisix/plugins/serverless/*.lua $(ENV_INST_LUADIR)/apisix/plugins/serverless/ - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/zipkin - $(INSTALL) apisix/plugins/zipkin/*.lua $(INST_LUADIR)/apisix/plugins/zipkin/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/zipkin + $(ENV_INSTALL) apisix/plugins/zipkin/*.lua $(ENV_INST_LUADIR)/apisix/plugins/zipkin/ - $(INSTALL) -d $(INST_LUADIR)/apisix/ssl/router - $(INSTALL) apisix/ssl/router/*.lua $(INST_LUADIR)/apisix/ssl/router/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/ssl/router + $(ENV_INSTALL) apisix/ssl/router/*.lua $(ENV_INST_LUADIR)/apisix/ssl/router/ - $(INSTALL) -d $(INST_LUADIR)/apisix/stream/plugins - $(INSTALL) apisix/stream/plugins/*.lua $(INST_LUADIR)/apisix/stream/plugins/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/plugins + $(ENV_INSTALL) apisix/stream/plugins/*.lua $(ENV_INST_LUADIR)/apisix/stream/plugins/ - $(INSTALL) -d $(INST_LUADIR)/apisix/stream/router - $(INSTALL) apisix/stream/router/*.lua $(INST_LUADIR)/apisix/stream/router/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/stream/router + $(ENV_INSTALL) apisix/stream/router/*.lua $(ENV_INST_LUADIR)/apisix/stream/router/ - $(INSTALL) -d $(INST_LUADIR)/apisix/utils - $(INSTALL) apisix/utils/*.lua $(INST_LUADIR)/apisix/utils/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/utils + $(ENV_INSTALL) apisix/utils/*.lua $(ENV_INST_LUADIR)/apisix/utils/ - $(INSTALL) bin/apisix $(INST_BINDIR)/apisix + $(ENV_INSTALL) bin/apisix $(ENV_INST_BINDIR)/apisix - $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/slslog - $(INSTALL) apisix/plugins/slslog/*.lua $(INST_LUADIR)/apisix/plugins/slslog/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/slslog + $(ENV_INSTALL) apisix/plugins/slslog/*.lua $(ENV_INST_LUADIR)/apisix/plugins/slslog/ ### test : Run the test case .PHONY: test -test: +test: runtime @$(call func_echo_status, "$@ -> [ Start ]") $(ENV_GIT) submodule update --init --recursive prove -I../test-nginx/lib -I./ -r -s t/ @@ -355,13 +352,15 @@ license-check: .PHONY: release-src release-src: compress-tar + @$(call func_echo_status, "$@ -> [ Start ]") gpg --batch --yes --armor --detach-sig $(project_release_name).tgz shasum -a 512 $(project_release_name).tgz > $(project_release_name).tgz.sha512 - mkdir -p release + $(call func_check_folder,release) mv $(project_release_name).tgz release/$(project_release_name).tgz mv $(project_release_name).tgz.asc release/$(project_release_name).tgz.asc mv $(project_release_name).tgz.sha512 release/$(project_release_name).tgz.sha512 + @$(call func_echo_success_status, "$@ -> [ Done ]") .PHONY: compress-tar @@ -379,7 +378,7 @@ compress-tar: ### container -### ci-env-up : launch ci env +### ci-env-up : CI env launch .PHONY: ci-env-up ci-env-up: @$(call func_echo_status, "$@ -> [ Start ]") @@ -387,7 +386,7 @@ ci-env-up: @$(call func_echo_success_status, "$@ -> [ Done ]") -### ci-env-ps : ci env ps +### ci-env-ps : CI env ps .PHONY: ci-env-ps ci-env-ps: @$(call func_echo_status, "$@ -> [ Start ]") @@ -395,7 +394,7 @@ ci-env-ps: @$(call func_echo_success_status, "$@ -> [ Done ]") -### ci-env-rebuild : ci env image rebuild +### ci-env-rebuild : CI env image rebuild .PHONY: ci-env-rebuild ci-env-rebuild: @$(call func_echo_status, "$@ -> [ Start ]") @@ -403,7 +402,7 @@ ci-env-rebuild: @$(call func_echo_success_status, "$@ -> [ Done ]") -### ci-env-down : destroy ci env +### ci-env-down : CI env destroy .PHONY: ci-env-down ci-env-down: @$(call func_echo_status, "$@ -> [ Start ]") diff --git a/ci/linux_apisix_current_luarocks_runner.sh b/ci/linux_apisix_current_luarocks_runner.sh index d93cf47a2fd3..a143d479d0a2 100755 --- a/ci/linux_apisix_current_luarocks_runner.sh +++ b/ci/linux_apisix_current_luarocks_runner.sh @@ -33,7 +33,7 @@ script() { sudo rm -rf /usr/local/apisix # install APISIX with local version - sudo luarocks install rockspec/apisix-master-0.rockspec --only-deps > build.log 2>&1 || (cat build.log && exit 1) + sudo luarocks install rockspec/apisix-master-0.rockspec --only-deps > build.log 2>&1 || (cat build.log && exit 1) sudo luarocks make rockspec/apisix-master-0.rockspec > build.log 2>&1 || (cat build.log && exit 1) mkdir cli_tmp && cd cli_tmp diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md index f9038a01d780..6dd2d6287467 100644 --- a/docs/en/latest/FAQ.md +++ b/docs/en/latest/FAQ.md @@ -75,7 +75,7 @@ For China mainland users, you can use the `luarocks.cn` as the luarocks server. We already provide a wrapper in the Makefile to simplify your job: ```bash -LUAROCKS_SERVER=https://luarocks.cn make deps +make deps ENV_LUAROCKS_SERVER=https://luarocks.cn ``` If using a proxy doesn't solve this problem, you can add `--verbose` option during installation to see exactly how slow it is. Excluding the first case, only the second that the `git` protocol is blocked. Then we can run `git config --global url."https://".insteadOf git://` to using the 'HTTPS' protocol instead of `git`. diff --git a/docs/zh/latest/FAQ.md b/docs/zh/latest/FAQ.md index 8aae9bfdfc76..5808f745f352 100644 --- a/docs/zh/latest/FAQ.md +++ b/docs/zh/latest/FAQ.md @@ -74,7 +74,7 @@ luarocks 服务。 运行 `luarocks config rocks_servers` 命令(这个命令 我们已经封装好了选择服务地址的操作: ```bash -LUAROCKS_SERVER=https://luarocks.cn make deps +make deps ENV_LUAROCKS_SERVER=https://luarocks.cn ``` 如果使用代理仍然解决不了这个问题,那可以在安装的过程中添加 `--verbose` 选项来查看具体是慢在什么地方。排除前面的 diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index 4f8557a17228..f3e37f08c0bc 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -87,10 +87,10 @@ build = { OPENSSL_LIBDIR="$(OPENSSL_LIBDIR)", }, install_variables = { - INST_PREFIX="$(PREFIX)", - INST_BINDIR="$(BINDIR)", - INST_LIBDIR="$(LIBDIR)", - INST_LUADIR="$(LUADIR)", - INST_CONFDIR="$(CONFDIR)", + ENV_INST_PREFIX="$(PREFIX)", + ENV_INST_BINDIR="$(BINDIR)", + ENV_INST_LIBDIR="$(LIBDIR)", + ENV_INST_LUADIR="$(LUADIR)", + ENV_INST_CONFDIR="$(CONFDIR)", }, } From fa8a34f72d4de45a42390d17ca27aa9f808deb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 22 Oct 2021 16:29:12 +0800 Subject: [PATCH 025/260] feat: initial wasm support (#5288) --- .github/workflows/build.yml | 8 + .gitignore | 1 + .licenserc.yaml | 4 +- README.md | 1 + apisix/cli/ngx_tpl.lua | 4 + apisix/cli/ops.lua | 27 ++- apisix/plugin.lua | 65 +++++-- apisix/wasm.lua | 120 +++++++++++++ conf/config-default.yaml | 6 + docs/en/latest/config.json | 4 + docs/en/latest/wasm.md | 96 ++++++++++ t/APISIX.pm | 1 + t/cli/test_wasm.sh | 66 +++++++ t/wasm/global-rule.t | 186 ++++++++++++++++++++ t/wasm/go.mod | 6 + t/wasm/go.sum | 8 + t/wasm/log/main.go | 81 +++++++++ t/wasm/route.t | 342 ++++++++++++++++++++++++++++++++++++ 18 files changed, 1008 insertions(+), 18 deletions(-) create mode 100644 apisix/wasm.lua create mode 100644 docs/en/latest/wasm.md create mode 100755 t/cli/test_wasm.sh create mode 100644 t/wasm/global-rule.t create mode 100644 t/wasm/go.mod create mode 100644 t/wasm/go.sum create mode 100644 t/wasm/log/main.go create mode 100644 t/wasm/route.t diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3597b40028f8..93597b6895d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,6 +72,14 @@ jobs: - name: Linux Get dependencies run: sudo apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl libpcre3 libpcre3-dev libldap2-dev + - name: Build wasm code + if: startsWith(matrix.os_name, 'linux_openresty') + run: | + export TINYGO_VER=0.20.0 + wget https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VER}/tinygo_${TINYGO_VER}_amd64.deb 2>/dev/null + sudo dpkg -i tinygo_${TINYGO_VER}_amd64.deb + cd t/wasm && find . -type f -name "main.go" | xargs -Ip tinygo build -o p.wasm -scheduler=none -target=wasi p + - name: Linux Before install run: sudo ./ci/${{ matrix.os_name }}_runner.sh before_install diff --git a/.gitignore b/.gitignore index d5a7d937edff..05fedad56be2 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ build-cache/ t/fuzzing/__pycache__/ boofuzz-results/ *.pyc +*.wasm # release tar package *.tgz release/* diff --git a/.licenserc.yaml b/.licenserc.yaml index 61e2647f94ca..9d71547c0206 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -35,8 +35,8 @@ header: - '**/*.log' # Exclude test toolkit files - 't/toolkit' - - 't/chaos/go.mod' - - 't/chaos/go.sum' + - 'go.mod' + - 'go.sum' # Exclude non-Apache licensed files - 'apisix/balancer/ewma.lua' # Exclude plugin-specific configuration files diff --git a/README.md b/README.md index 248c868fd5cb..be4b386a8bee 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - **Highly scalable** - [Custom plugins](docs/en/latest/plugin-develop.md): Allows hooking of common phases, such as `rewrite`, `access`, `header filter`, `body filter` and `log`, also allows to hook the `balancer` stage. - [Plugin can be written in Java/Go/Python](docs/en/latest/external-plugin.md) + - [Plugin can be written with Proxy WASM SDK](docs/en/latest/wasm.md) - Custom load balancing algorithms: You can use custom load balancing algorithms during the `balancer` phase. - Custom routing: Support users to implement routing algorithms themselves. diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 80760b7d9159..f5fa5d6ee06d 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -348,6 +348,10 @@ http { } {% end %} + {% if wasm then %} + wasm_vm wasmtime; + {% end %} + init_by_lua_block { require "resty.core" {% if lua_module_hook then %} diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 68568680b57b..ed9abe92556a 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -338,7 +338,31 @@ local config_schema = { } } } - } + }, + wasm = { + type = "object", + properties = { + plugins = { + type = "array", + minItems = 1, + items = { + type = "object", + properties = { + name = { + type = "string" + }, + file = { + type = "string" + }, + priority = { + type = "integer" + } + }, + required = {"name", "file", "priority"} + } + } + } + }, } } @@ -752,6 +776,7 @@ Please modify "admin_key" in conf/config.yaml . for k,v in pairs(yaml_conf.nginx_config) do sys_conf[k] = v end + sys_conf["wasm"] = yaml_conf.wasm local wrn = sys_conf["worker_rlimit_nofile"] diff --git a/apisix/plugin.lua b/apisix/plugin.lua index 1e060cd6e11f..efdffe47d40e 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -18,6 +18,7 @@ local require = require local core = require("apisix.core") local config_util = require("apisix.core.config_util") local enable_debug = require("apisix.debug").enable_debug +local wasm = require("apisix.wasm") local ngx_exit = ngx.exit local pkg_loaded = package.loaded local sort_tab = table.sort @@ -67,9 +68,16 @@ local function sort_plugin(l, r) end -local function unload_plugin(name, is_stream_plugin) +local PLUGIN_TYPE_HTTP = 1 +local PLUGIN_TYPE_STREAM = 2 +local PLUGIN_TYPE_HTTP_WASM = 3 +local function unload_plugin(name, plugin_type) + if plugin_type == PLUGIN_TYPE_HTTP_WASM then + return + end + local pkg_name = "apisix.plugins." .. name - if is_stream_plugin then + if plugin_type == PLUGIN_TYPE_STREAM then pkg_name = "apisix.stream.plugins." .. name end @@ -82,13 +90,21 @@ local function unload_plugin(name, is_stream_plugin) end -local function load_plugin(name, plugins_list, is_stream_plugin) - local pkg_name = "apisix.plugins." .. name - if is_stream_plugin then - pkg_name = "apisix.stream.plugins." .. name +local function load_plugin(name, plugins_list, plugin_type) + local ok, plugin + if plugin_type == PLUGIN_TYPE_HTTP_WASM then + -- for wasm plugin, we pass the whole attrs instead of name + ok, plugin = wasm.require(name) + name = name.name + else + local pkg_name = "apisix.plugins." .. name + if plugin_type == PLUGIN_TYPE_STREAM then + pkg_name = "apisix.stream.plugins." .. name + end + + ok, plugin = pcall(require, pkg_name) end - local ok, plugin = pcall(require, pkg_name) if not ok then core.log.error("failed to load plugin [", name, "] err: ", plugin) return @@ -140,25 +156,39 @@ local function load_plugin(name, plugins_list, is_stream_plugin) end -local function load(plugin_names) +local function load(plugin_names, wasm_plugin_names) local processed = {} for _, name in ipairs(plugin_names) do if processed[name] == nil then processed[name] = true end end + for _, attrs in ipairs(wasm_plugin_names) do + if processed[attrs.name] == nil then + processed[attrs.name] = attrs + end + end core.log.warn("new plugins: ", core.json.delay_encode(processed)) - for name in pairs(local_plugins_hash) do - unload_plugin(name) + for name, plugin in pairs(local_plugins_hash) do + local ty = PLUGIN_TYPE_HTTP + if plugin.type == "wasm" then + ty = PLUGIN_TYPE_HTTP_WASM + end + unload_plugin(name, ty) end core.table.clear(local_plugins) core.table.clear(local_plugins_hash) - for name in pairs(processed) do - load_plugin(name, local_plugins) + for name, value in pairs(processed) do + local ty = PLUGIN_TYPE_HTTP + if type(value) == "table" then + ty = PLUGIN_TYPE_HTTP_WASM + name = value + end + load_plugin(name, local_plugins, ty) end -- sort by plugin's priority @@ -192,14 +222,14 @@ local function load_stream(plugin_names) core.log.warn("new plugins: ", core.json.delay_encode(processed)) for name in pairs(stream_local_plugins_hash) do - unload_plugin(name, true) + unload_plugin(name, PLUGIN_TYPE_STREAM) end core.table.clear(stream_local_plugins) core.table.clear(stream_local_plugins_hash) for name in pairs(processed) do - load_plugin(name, stream_local_plugins, true) + load_plugin(name, stream_local_plugins, PLUGIN_TYPE_STREAM) end -- sort by plugin's priority @@ -260,7 +290,12 @@ function _M.load(config) if not http_plugin_names then core.log.error("failed to read plugin list from local file") else - local ok, err = load(http_plugin_names) + local wasm_plugin_names = {} + if local_conf.wasm then + wasm_plugin_names = local_conf.wasm.plugins + end + + local ok, err = load(http_plugin_names, wasm_plugin_names) if not ok then core.log.error("failed to load plugins: ", err) end diff --git a/apisix/wasm.lua b/apisix/wasm.lua new file mode 100644 index 000000000000..d018e5973d42 --- /dev/null +++ b/apisix/wasm.lua @@ -0,0 +1,120 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +local core = require("apisix.core") +local support_wasm, wasm = pcall(require, "resty.proxy-wasm") +local concat = table.concat + + +local schema = { + type = "object", + properties = { + conf = { + type = "string" + }, + }, + required = {"conf"} +} +local _M = {} + + +local function check_schema(conf) + return core.schema.check(schema, conf) +end + + +local get_plugin_ctx_key +do + local key_buf = { + nil, + nil, + } + + function get_plugin_ctx_key(ctx) + key_buf[1] = ctx.conf_type + key_buf[2] = ctx.conf_id + return concat(key_buf, "#", 1, 2) + end +end + +local function fetch_plugin_ctx(conf, ctx, plugin) + if not conf.plugin_ctxs then + conf.plugin_ctxs = {} + end + + local ctxs = conf.plugin_ctxs + local key = get_plugin_ctx_key(ctx) + local plugin_ctx = ctxs[key] + local err + if not plugin_ctx then + plugin_ctx, err = wasm.on_configure(plugin, conf.conf) + if not plugin_ctx then + return nil, err + end + + ctxs[key] = plugin_ctx + end + + return plugin_ctx +end + + +local function access_wrapper(self, conf, ctx) + local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin) + if not plugin_ctx then + core.log.error("failed to init wasm plugin ctx: ", err) + return 503 + end + + local ok, err = wasm.on_http_request_headers(plugin_ctx) + if not ok then + core.log.error("failed to run wasm plugin: ", err) + return 503 + end +end + + +function _M.require(attrs) + if not support_wasm then + return nil, "need to build APISIX-OpenResty to support wasm" + end + + local name = attrs.name + local priority = attrs.priority + local plugin, err = wasm.load(name, attrs.file) + if not plugin then + return nil, err + end + + local mod = { + version = 0.1, + name = name, + priority = priority, + schema = schema, + check_schema = check_schema, + plugin = plugin, + type = "wasm", + } + mod.access = function (conf, ctx) + return access_wrapper(mod, conf, ctx) + end + + -- the returned values need to be the same as the Lua's 'require' + return true, mod +end + + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 32ed56b87c9b..54e760d9b48b 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -361,6 +361,12 @@ stream_plugins: # sorted by priority - mqtt-proxy # priority: 1000 # <- recommend to use priority (0, 100) for your custom plugins +#wasm: + #plugins: + #- name: wasm_log + #priority: 7999 + #file: t/wasm/log/main.go.wasm + plugin_attr: log-rotate: interval: 3600 # rotate interval (unit: second) diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 904583330e5a..deabb64d9e26 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -213,6 +213,10 @@ "type": "doc", "id": "external-plugin" }, + { + "type": "doc", + "id": "wasm" + }, { "type": "doc", "id": "plugin-interceptors" diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md new file mode 100644 index 000000000000..5f13d7ea1bfc --- /dev/null +++ b/docs/en/latest/wasm.md @@ -0,0 +1,96 @@ +--- +title: WASM +--- + + + +APISIX supports WASM plugins written with [Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks). + +This plugin requires APISIX to run on [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix), and is under construction. +Currently, only a few APIs are implemented. Please follow [wasm-nginx-module](https://github.com/api7/wasm-nginx-module) to know the progress. + +## Programming model + +The plugin supports the follwing concepts from Proxy WASM: + +``` + Wasm Virtual Machine +┌────────────────────────────────────────────────────────────────┐ +│ Your Plugin │ +│ │ │ +│ │ 1: 1 │ +│ │ 1: N │ +│ VMContext ────────── PluginContext │ +│ ╲ 1: N │ +│ ╲ │ +│ ╲ HttpContext │ +│ (Http stream) │ +└────────────────────────────────────────────────────────────────┘ +``` + +* All plugins run in the same WASM VM, like the Lua plugin in the Lua VM +* Each plugin has its own VMContext (the root ctx) +* Each configured route/global rules has its own PluginContext (the plugin ctx). +For example, if we have a service configuring with WASM plugin, and two routes inherit from it, +there will be two plugin ctxs. +* Each HTTP request which hits the configuration will have its own HttpContext (the HTTP ctx). +For example, if we configure both global rules and route, the HTTP request will +have two HTTP ctxs, one for the plugin ctx from global rules and the other for the +plugin ctx from route. + +## How to use + +First of all, we need to define the plugin in `config.yaml`: + +```yaml +wasm: + plugins: + - name: wasm_log # the name of the plugin + priority: 7999 # priority + file: t/wasm/log/main.go.wasm # the path of `.wasm` file +``` + +That's all. Now you can use the wasm plugin as a regular plugin. + +For example, enable this plugin on the specified route: + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "wasm_log": { + "conf": "blahblah" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +Attributes below can be configured in the plugin: + +| Name | Type | Requirement | Default | Valid | Description | +| --------------------------------------| ------------| -------------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| conf | string | required | | | the plugin ctx configuration which can be fetched via Proxy WASM SDK | diff --git a/t/APISIX.pm b/t/APISIX.pm index 83e28c810a7e..da9a46e75e84 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -212,6 +212,7 @@ my $a6_ngx_directives = ""; if ($version =~ m/\/apisix-nginx-module/) { $a6_ngx_directives = <<_EOC_; apisix_delay_client_max_body_check on; + wasm_vm wasmtime; _EOC_ } diff --git a/t/cli/test_wasm.sh b/t/cli/test_wasm.sh new file mode 100755 index 000000000000..a8e5584a8e8c --- /dev/null +++ b/t/cli/test_wasm.sh @@ -0,0 +1,66 @@ +#!/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. +# + +. ./t/cli/common.sh + +exit_if_not_customed_nginx + +echo ' +wasm: + plugins: + - name: wasm_log + file: t/wasm/log/main.go.wasm +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'property "priority" is required'; then + echo "failed: priority is required" + exit 1 +fi + +echo ' +wasm: + plugins: + - name: wasm_log + priority: 888 +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'property "file" is required'; then + echo "failed: file is required" + exit 1 +fi + +echo "passed: wasm configuration is validated" + +echo ' +wasm: + plugins: + - name: wasm_log + priority: 7999 + file: t/wasm/log/main.go.wasm + ' > conf/config.yaml + +make init +if ! grep "wasm_vm " conf/nginx.conf; then + echo "failed: wasm isn't enabled" + exit 1 +fi + +echo "passed: wasm is enabled" diff --git a/t/wasm/global-rule.t b/t/wasm/global-rule.t new file mode 100644 index 000000000000..8dd66cb4e15f --- /dev/null +++ b/t/wasm/global-rule.t @@ -0,0 +1,186 @@ +# +# 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; + +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'); +} + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $extra_yaml_config = <<_EOC_; +wasm: + plugins: + - name: wasm_log + priority: 7999 + file: t/wasm/log/main.go.wasm + - name: wasm_log2 + priority: 7998 + file: t/wasm/log/main.go.wasm +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/global_rules/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_log": { + "conf": "blahblah" + }, + "wasm_log2": { + "conf": "zzz" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit +--- request +GET /hello +--- grep_error_log eval +qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/ +--- grep_error_log_out +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 1 with conf zzz in http ctx 2 + + + +=== TEST 3: global rule + route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_log2": { + "conf": "www" + } + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: hit +--- request +GET /hello +--- grep_error_log eval +qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/ +--- grep_error_log_out +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 1 with conf zzz in http ctx 2 +run plugin ctx 3 with conf www in http ctx 4 + + + +=== TEST 5: delete global rule +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/global_rules/1', ngx.HTTP_DELETE) + if code >= 300 then + ngx.status = code + end + + ngx.say(body) + } + } +--- response_body +passed diff --git a/t/wasm/go.mod b/t/wasm/go.mod new file mode 100644 index 000000000000..9a875c8144c9 --- /dev/null +++ b/t/wasm/go.mod @@ -0,0 +1,6 @@ +module github.com/api7/wasm-nginx-module + +go 1.15 + +require github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31 +//replace github.com/tetratelabs/proxy-wasm-go-sdk => ../proxy-wasm-go-sdk diff --git a/t/wasm/go.sum b/t/wasm/go.sum new file mode 100644 index 000000000000..599f22615046 --- /dev/null +++ b/t/wasm/go.sum @@ -0,0 +1,8 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31 h1:V3GXN5nayOdIU3NypbxVegGFCVGm78qOA8Q7wkeudy8= +github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31/go.mod h1:qZ+4i6e2wHlhnhgpH0VG4QFzqd2BEvQbQFU0npt2e2k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/t/wasm/log/main.go b/t/wasm/log/main.go new file mode 100644 index 000000000000..2880b0988bb0 --- /dev/null +++ b/t/wasm/log/main.go @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package main + +import ( + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" +) + +func main() { + proxywasm.SetVMContext(&vmContext{}) +} + +type vmContext struct { + // Embed the default VM context here, + // so that we don't need to reimplement all the methods. + types.DefaultVMContext +} + +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{contextID: contextID} +} + +type pluginContext struct { + // Embed the default plugin context here, + // so that we don't need to reimplement all the methods. + types.DefaultPluginContext + conf string + contextID uint32 +} + +func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + data, err := proxywasm.GetPluginConfiguration() + if err != nil { + proxywasm.LogCriticalf("error reading plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + ctx.conf = string(data) + return types.OnPluginStartStatusOK +} + +func (ctx *pluginContext) OnPluginDone() bool { + proxywasm.LogInfo("do clean up...") + return true +} + +func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpLifecycle{pluginCtxID: ctx.contextID, conf: ctx.conf, contextID: contextID} +} + +type httpLifecycle struct { + // Embed the default http context here, + // so that we don't need to reimplement all the methods. + types.DefaultHttpContext + pluginCtxID uint32 + contextID uint32 + conf string +} + +func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + proxywasm.LogWarnf("run plugin ctx %d with conf %s in http ctx %d", + ctx.pluginCtxID, ctx.conf, ctx.contextID) + // TODO: support access/modify http request headers + return types.ActionContinue +} diff --git a/t/wasm/route.t b/t/wasm/route.t new file mode 100644 index 000000000000..69f43d39a954 --- /dev/null +++ b/t/wasm/route.t @@ -0,0 +1,342 @@ +# +# 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; + +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'); +} + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $extra_yaml_config = <<_EOC_; +wasm: + plugins: + - name: wasm_log + priority: 7999 + file: t/wasm/log/main.go.wasm + - name: wasm_log2 + priority: 7998 + file: t/wasm/log/main.go.wasm +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: check schema +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + for _, case in ipairs({ + {input = { + }}, + {input = { + conf = {} + }}, + }) do + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + { + id = "1", + uri = "/echo", + upstream = { + type = "roundrobin", + nodes = {} + }, + plugins = { + wasm_log = case.input + } + } + ) + ngx.say(json.decode(body).error_msg) + end + } + } +--- response_body +failed to check the configuration of plugin wasm_log err: property "conf" is required +failed to check the configuration of plugin wasm_log err: property "conf" validation failed: wrong type: expected string, got table + + + +=== TEST 2: sanity +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_log": { + "conf": "blahblah" + }, + "wasm_log2": { + "conf": "zzz" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: hit +--- request +GET /hello +--- grep_error_log eval +qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/ +--- grep_error_log_out +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 1 with conf zzz in http ctx 2 + + + +=== TEST 4: plugin from service +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_log": { + "id": "log", + "conf": "blahblah" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "service_id": "1", + "hosts": ["foo.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "service_id": "1", + "hosts": ["bar.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: hit +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + + for i = 1, 4 do + local host = "foo.com" + if i % 2 == 0 then + host = "bar.com" + end + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {host = host}}) + if not res then + ngx.say(err) + return + end + end + } + } +--- grep_error_log eval +qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/ +--- grep_error_log_out +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 3 with conf blahblah in http ctx 4 +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 3 with conf blahblah in http ctx 4 + + + +=== TEST 6: plugin from plugin_config +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "wasm_log": { + "id": "log", + "conf": "blahblah" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello", + "plugin_config_id": "1", + "hosts": ["foo.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello", + "plugin_config_id": "1", + "hosts": ["bar.com"] + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 7: hit +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + + for i = 1, 4 do + local host = "foo.com" + if i % 2 == 0 then + host = "bar.com" + end + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {host = host}}) + if not res then + ngx.say(err) + return + end + end + } + } +--- grep_error_log eval +qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/ +--- grep_error_log_out +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 3 with conf blahblah in http ctx 4 +run plugin ctx 1 with conf blahblah in http ctx 2 +run plugin ctx 3 with conf blahblah in http ctx 4 From 05b36d36176b7f06c04a1aed82b9987f6cb6996f Mon Sep 17 00:00:00 2001 From: zdzh Date: Fri, 22 Oct 2021 18:02:15 +0800 Subject: [PATCH 026/260] fix(ext-plugin): don't use stale key (#5309) Co-authored-by: zdzh --- apisix/plugins/ext-plugin/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index 72d89fcb6490..1a8fc9cdb40a 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -66,6 +66,7 @@ local type = type local events_list local lrucache = core.lrucache.new({ type = "plugin", + invalid_stale = true, ttl = helper.get_conf_token_cache_time(), }) local shdict_name = "ext-plugin" From 8c3bbe0ede4d615bf9bc9ae9087690dd017351d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 24 Oct 2021 11:34:08 +0800 Subject: [PATCH 027/260] chore: the buf doesn't bring any performance improvement but make code complex (#5317) --- apisix/core/lrucache.lua | 44 +++++++++++++--------------------------- apisix/wasm.lua | 15 ++------------ 2 files changed, 16 insertions(+), 43 deletions(-) diff --git a/apisix/core/lrucache.lua b/apisix/core/lrucache.lua index 9bc5c6fca133..97c23ecec432 100644 --- a/apisix/core/lrucache.lua +++ b/apisix/core/lrucache.lua @@ -19,7 +19,6 @@ local lru_new = require("resty.lrucache").new local resty_lock = require("resty.lock") local log = require("apisix.core.log") local tostring = tostring -local concat = table.concat local ngx = ngx local get_phase = ngx.get_phase @@ -139,39 +138,24 @@ end global_lru_fun = new_lru_fun() -local plugin_ctx, plugin_ctx_id -do - local key_buf = { - nil, - nil, - nil, - nil, - } - - local function plugin_ctx_key_and_ver(api_ctx, extra_key) - key_buf[1] = api_ctx.conf_type - key_buf[2] = api_ctx.conf_id - - local key - if extra_key then - key_buf[3] = extra_key - key = concat(key_buf, "#", 1, 3) - else - key = concat(key_buf, "#", 1, 2) - end +local function plugin_ctx_key_and_ver(api_ctx, extra_key) + local key = api_ctx.conf_type .. "#" .. api_ctx.conf_id - return key, api_ctx.conf_version + if extra_key then + key = key .. "#" .. extra_key end - function plugin_ctx(lrucache, api_ctx, extra_key, create_obj_func, ...) - local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key) - return lrucache(key, ver, create_obj_func, ...) - end + return key, api_ctx.conf_version +end - function plugin_ctx_id(api_ctx, extra_key) - local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key) - return key .. "#" .. ver - end +local function plugin_ctx(lrucache, api_ctx, extra_key, create_obj_func, ...) + local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key) + return lrucache(key, ver, create_obj_func, ...) +end + +local function plugin_ctx_id(api_ctx, extra_key) + local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key) + return key .. "#" .. ver end diff --git a/apisix/wasm.lua b/apisix/wasm.lua index d018e5973d42..c99731a99aaa 100644 --- a/apisix/wasm.lua +++ b/apisix/wasm.lua @@ -16,7 +16,6 @@ -- local core = require("apisix.core") local support_wasm, wasm = pcall(require, "resty.proxy-wasm") -local concat = table.concat local schema = { @@ -36,18 +35,8 @@ local function check_schema(conf) end -local get_plugin_ctx_key -do - local key_buf = { - nil, - nil, - } - - function get_plugin_ctx_key(ctx) - key_buf[1] = ctx.conf_type - key_buf[2] = ctx.conf_id - return concat(key_buf, "#", 1, 2) - end +local function get_plugin_ctx_key(ctx) + return ctx.conf_type .. "#" .. ctx.conf_id end local function fetch_plugin_ctx(conf, ctx, plugin) From bddec4189ad4a19225c8d1b54dd07bde28fa7be0 Mon Sep 17 00:00:00 2001 From: Huei Feng <695979933@qq.com> Date: Sun, 24 Oct 2021 12:14:24 +0800 Subject: [PATCH 028/260] docs: add ctrl cloud to powered-by.md (#5315) Co-authored-by: leslie <59061168+leslie-tsang@users.noreply.github.com> --- powered-by.md | 1 + 1 file changed, 1 insertion(+) diff --git a/powered-by.md b/powered-by.md index cebfcc84c8dd..99fef66f98ce 100644 --- a/powered-by.md +++ b/powered-by.md @@ -32,6 +32,7 @@ Users are encouraged to add themselves to this page, [issue](https://github.com/ 1. cunw 湖南新云网 1. Chaolian 超链云商 1. CCB Fintech 建信金科 +1. CTRL 开创云 1. 51tiangou 大商天狗 1. DaoCloud 1. dasouche 大搜车 From ded7c44393e734ef00e6b2dbb999d6596384d744 Mon Sep 17 00:00:00 2001 From: jackfu Date: Sun, 24 Oct 2021 19:07:42 +0800 Subject: [PATCH 029/260] fix(zipkin): response_span doesn't have correct start time (#5295) Co-authored-by: jack.fu --- apisix/plugins/zipkin.lua | 5 ++--- t/plugin/zipkin2.t | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apisix/plugins/zipkin.lua b/apisix/plugins/zipkin.lua index e292b62ae64f..361f2c71ddf8 100644 --- a/apisix/plugins/zipkin.lua +++ b/apisix/plugins/zipkin.lua @@ -240,15 +240,14 @@ function _M.header_filter(conf, ctx) local end_time = opentracing.tracer:time() if conf.span_version == ZIPKIN_SPAN_VER_1 then - ctx.HEADER_FILTER_END_TIME = end_time if opentracing.proxy_span then opentracing.body_filter_span = opentracing.proxy_span:start_child_span( - "apisix.body_filter", ctx.HEADER_FILTER_END_TIME) + "apisix.body_filter", end_time) end else opentracing.proxy_span:finish(end_time) opentracing.response_span = opentracing.request_span:start_child_span( - "apisix.response_span", ctx.HEADER_FILTER_END_TIME) + "apisix.response_span", end_time) end end diff --git a/t/plugin/zipkin2.t b/t/plugin/zipkin2.t index 7b0edef51a70..9840092943e2 100644 --- a/t/plugin/zipkin2.t +++ b/t/plugin/zipkin2.t @@ -31,6 +31,25 @@ add_block_preprocessor(sub { if (!$block->no_error_log && !$block->error_log) { $block->set_value("no_error_log", "[error]\n[alert]"); } + + my $extra_init_by_lua = <<_EOC_; + local new = require("opentracing.tracer").new + local tracer_mt = getmetatable(new()).__index + local orig_func = tracer_mt.start_span + tracer_mt.start_span = function (...) + local orig = orig_func(...) + local mt = getmetatable(orig).__index + local old_start_child_span = mt.start_child_span + mt.start_child_span = function(self, name, time) + ngx.log(ngx.WARN, "zipkin start_child_span ", name, " time: ", time) + return old_start_child_span(self, name, time) + end + return orig + end +_EOC_ + + $block->set_value("extra_init_by_lua", $extra_init_by_lua); + }); run_tests; @@ -83,6 +102,9 @@ x-b3-sampled: 1 b3: --- error_log new span context: trace id: 80f198ee56343ba864fe8b2a57d3eff7, span id: e457b5a2e4d86bd1, parent span id: 05e3ac9a4f6e3b90 +--- grep_error_log eval +qr/zipkin start_child_span apisix.response_span time: nil/ +--- grep_error_log_out @@ -186,3 +208,6 @@ GET /t --- request GET /opentracing --- wait: 10 +--- grep_error_log eval +qr/zipkin start_child_span apisix.response_span time: nil/ +--- grep_error_log_out From a960f85ca4610aff7807e722ecc5862cae76180d Mon Sep 17 00:00:00 2001 From: Bisakh Date: Mon, 25 Oct 2021 06:29:44 +0530 Subject: [PATCH 030/260] ci: the test framework requires curl with http2 support (#5308) --- ci/centos7-ci.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index 744bed77c543..9b6a48d63dee 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -23,8 +23,10 @@ install_dependencies() { # install development tools yum install -y wget tar gcc automake autoconf libtool make unzip \ - curl git which sudo openldap-devel + git which sudo openldap-devel + # curl with http2 + wget https://github.com/moparisthebest/static-curl/releases/download/v7.79.1/curl-amd64 -O /usr/bin/curl # install openresty to make apisix's rpm test work yum install -y yum-utils && yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo yum install -y openresty openresty-debug openresty-openssl111-debug-devel pcre pcre-devel From a348ffffe47649b504a42e3d5a73431d1add71d9 Mon Sep 17 00:00:00 2001 From: Zhendong Qi <88528414+zhendongcmss@users.noreply.github.com> Date: Mon, 25 Oct 2021 10:34:25 +0800 Subject: [PATCH 031/260] feat(proxy-rewrite): add method proxy (#5292) Co-authored-by: qizhendong --- apisix/plugins/proxy-rewrite.lua | 22 ++++ docs/en/latest/plugins/proxy-rewrite.md | 1 + docs/zh/latest/plugins/proxy-rewrite.md | 1 + t/lib/server.lua | 1 + t/plugin/proxy-rewrite3.t | 153 ++++++++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 t/plugin/proxy-rewrite3.t diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua index 397d6d055545..ca9210e14146 100644 --- a/apisix/plugins/proxy-rewrite.lua +++ b/apisix/plugins/proxy-rewrite.lua @@ -24,6 +24,19 @@ local re_sub = ngx.re.sub local sub_str = string.sub local str_find = core.string.find +local switch_map = {GET = ngx.HTTP_GET, POST = ngx.HTTP_POST, PUT = ngx.HTTP_PUT, + HEAD = ngx.HTTP_HEAD, DELETE = ngx.HTTP_DELETE, + OPTIONS = ngx.HTTP_OPTIONS, MKCOL = ngx.HTTP_MKCOL, + COPY = ngx.HTTP_COPY, MOVE = ngx.HTTP_MOVE, + PROPFIND = ngx.HTTP_PROPFIND, LOCK = ngx.HTTP_LOCK, + UNLOCK = ngx.HTTP_UNLOCK, PATCH = ngx.HTTP_PATCH, + TRACE = ngx.HTTP_TRACE, + } +local schema_method_enum = {} +for key in pairs(switch_map) do + core.table.insert(schema_method_enum, key) +end + local schema = { type = "object", properties = { @@ -34,6 +47,11 @@ local schema = { maxLength = 4096, pattern = [[^\/.*]], }, + method = { + description = "proxy route method", + type = "string", + enum = schema_method_enum + }, regex_uri = { description = "new uri that substitute from client uri " .. "for upstream, lower priority than uri property", @@ -196,6 +214,10 @@ function _M.rewrite(conf, ctx) ngx.req.set_header(conf.headers_arr[i], core.utils.resolve_var(conf.headers_arr[i+1], ctx.var)) end + + if conf.method then + ngx.req.set_method(switch_map[conf.method]) + end end end -- do diff --git a/docs/en/latest/plugins/proxy-rewrite.md b/docs/en/latest/plugins/proxy-rewrite.md index f5adf1f0ca51..a538ecba35c8 100644 --- a/docs/en/latest/plugins/proxy-rewrite.md +++ b/docs/en/latest/plugins/proxy-rewrite.md @@ -39,6 +39,7 @@ The `proxy-rewrite` is an upstream proxy information rewriting plugin, which sup | --------- | ------------- | ----------- | ------- | ----------------- | ------------------------------------------------------------ | | scheme | string | optional | "http" | ["http", "https"] | Upstream new `schema` forwarding protocol. This option is deprecated. It's recommended to set the proxy `scheme` in the Upstream object's `scheme` field instead.| | uri | string | optional | | | Upstream new `uri` forwarding address. Supports the use of [Nginx variables](https://nginx.org/en/docs/http/ngx_http_core_module.html). Variables must start with `$`, such as `$arg_name`. | +| method | string | optional | | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS","MKCOL", "COPY", "MOVE", "PROPFIND", "PROPFIND","LOCK", "UNLOCK", "PATCH", "TRACE"] | rewrite the HTTP method.| | regex_uri | array[string] | optional | | | Upstream new `uri` forwarding address. Use regular expression to match URL from client, when the match is successful, the URL template will be forwarded upstream. If the match is not successful, the URL from the client will be forwarded to the upstream. When `uri` and `regex_uri` are both exist, `uri` is used first. For example: [" ^/iresty/(.*)/(.*)/(.*)", "/$1-$2-$3"], the first element represents the matching regular expression and the second element represents the URL template that is forwarded to the upstream. | | host | string | optional | | | Upstream new `host` forwarding address, example `iresty.com`. | | headers | object | optional | | | Forward to the new `headers` of the upstream, can set up multiple. If it exists, will rewrite the header, otherwise will add the header. You can set the corresponding value to an empty string to remove a header. Support the use of Nginx variables. Need to start with `$`, such as `client_addr: $remote_addr`: it means that the request header `client_addr` is the client IP. | diff --git a/docs/zh/latest/plugins/proxy-rewrite.md b/docs/zh/latest/plugins/proxy-rewrite.md index e5449098a8a0..d30a270dda70 100644 --- a/docs/zh/latest/plugins/proxy-rewrite.md +++ b/docs/zh/latest/plugins/proxy-rewrite.md @@ -39,6 +39,7 @@ proxy-rewrite 是上游代理信息重写插件,支持对 `scheme`、`uri`、` | --------- | ------------- | ----------- | ------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | scheme | string | 可选 | "http" | ["http", "https"] | 不推荐使用。应该在 Upstream 的 scheme 字段设置上游的 scheme。 | uri | string | 可选 | | | 转发到上游的新 `uri` 地址。 | +| method | string | 可选 | | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS","MKCOL", "COPY", "MOVE", "PROPFIND", "PROPFIND","LOCK", "UNLOCK", "PATCH", "TRACE"] | 将route的请求方法代理为该请求方法。 | | regex_uri | array[string] | 可选 | | | 转发到上游的新 `uri` 地址, 使用正则表达式匹配来自客户端的uri,当匹配成功后使用模板替换转发到上游的uri, 未匹配成功时将客户端请求的uri转发至上游。当`uri`和`regex_uri`同时存在时,`uri`优先被使用。例如:["^/iresty/(.*)/(.*)/(.*)","/$1-$2-$3"] 第一个元素代表匹配来自客户端请求的uri正则表达式,第二个元素代表匹配成功后转发到上游的uri模板。 | | host | string | 可选 | | | 转发到上游的新 `host` 地址,例如:`iresty.com` 。 | | headers | object | 可选 | | | 转发到上游的新`headers`,可以设置多个。头信息如果存在将重写,不存在则添加。想要删除某个 header 的话,把对应的值设置为空字符串即可。支持使用 Nginx 的变量,需要以 `$` 开头,如 `client_addr: $remote_addr` :表示请求头 `client_addr` 为客户端IP。 | diff --git a/t/lib/server.lua b/t/lib/server.lua index d72a3088e098..02550d68ec99 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -76,6 +76,7 @@ function _M.plugin_proxy_rewrite() ngx.say("uri: ", ngx.var.uri) ngx.say("host: ", ngx.var.host) ngx.say("scheme: ", ngx.var.scheme) + ngx.log(ngx.WARN, "plugin_proxy_rewrite get method: ", ngx.req.get_method()) end diff --git a/t/plugin/proxy-rewrite3.t b/t/plugin/proxy-rewrite3.t new file mode 100644 index 000000000000..31364d08ecf3 --- /dev/null +++ b/t/plugin/proxy-rewrite3.t @@ -0,0 +1,153 @@ +# +# 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) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set route(rewrite method) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "plugins": { + "proxy-rewrite": { + "uri": "/plugin_proxy_rewrite", + "method": "POST", + "scheme": "http", + "host": "apisix.iresty.com" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit route(upstream uri: should be /hello) +--- request +GET /hello +--- grep_error_log_out +plugin_proxy_rewrite get method: POST + + + +=== TEST 3: set route(update rewrite method) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["GET"], + "plugins": { + "proxy-rewrite": { + "uri": "/plugin_proxy_rewrite", + "method": "GET", + "scheme": "http", + "host": "apisix.iresty.com" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: hit route(upstream uri: should be /hello) +--- request +GET /hello +--- grep_error_log_out +plugin_proxy_rewrite get method: GET + + + +=== TEST 5: wrong value of method key +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.proxy-rewrite") + local ok, err = plugin.check_schema({ + uri = '/apisix/home', + method = 'GET1', + host = 'apisix.iresty.com', + scheme = 'http' + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +property "method" validation failed: matches none of the enum values +done From 1b3000556ea6e6e5008b28d5f0b9375f6eb433f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 25 Oct 2021 12:43:12 +0800 Subject: [PATCH 032/260] docs: record the release steps (#5324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 琚致远 --- MAINTAIN.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 MAINTAIN.md diff --git a/MAINTAIN.md b/MAINTAIN.md new file mode 100644 index 000000000000..96de8d5c154f --- /dev/null +++ b/MAINTAIN.md @@ -0,0 +1,51 @@ + + +## Release steps + +### Release patch version + +1. Create a pull request (contains the changelog and version change) to master, merge it +2. Create a pull request (contains the backport commits, and the change in step 1) to minor branch +3. Merge it into minor branch +4. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created +via `VERSION=x.y.z make release-src` +5. Send the vote email to dev@apisix.apache.org +6. When the vote is passed, send the vote result email to dev@apisix.apache.org +7. Move the vote artifact to Apache's apisix repo +8. Create a GitHub release from the minor branch +9. Update [APISIX's website](https://github.com/apache/apisix-website/blob/master/website/docusaurus.config.js#L110-L123) +10. Update APISIX docker +11. Update APISIX rpm package +12. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org + +### Release minor version + +1. Create a minor branch, and create pull request to master branch from it +2. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created +via `VERSION=x.y.z make release-src` +3. Send the vote email to dev@apisix.apache.org +4. When the vote is passed, send the vote result email to dev@apisix.apache.org +5. Move the vote artifact to Apache's apisix repo +6. Create a GitHub release from the minor branch +7. Merge the pull request into master branch +8. Update APISIX website +9. Update APISIX docker +10. Update APISIX rpm package +11. Send the ANNOUNCE email to dev@apisix.apache.org & announce@apache.org From b507601658e212ee315f414d2cab418c9be57aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 25 Oct 2021 12:43:45 +0800 Subject: [PATCH 033/260] ci: don't run full tests when we just change *.md (#5323) --- .github/workflows/build.yml | 2 ++ .github/workflows/centos7-ci.yml | 2 ++ .github/workflows/chaos.yml | 1 + .github/workflows/cli.yml | 2 ++ .github/workflows/code-lint.yml | 1 + .github/workflows/fuzzing-ci.yaml | 2 ++ 6 files changed, 10 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93597b6895d5..16453f34e1ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,12 @@ on: branches: [master, 'release/**'] paths-ignore: - 'docs/**' + - '**/*.md' pull_request: branches: [master] paths-ignore: - 'docs/**' + - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_number || github.ref }} diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index c3cd3a21afde..2f4ba8c5a560 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -5,10 +5,12 @@ on: branches: [master, 'release/**'] paths-ignore: - 'docs/**' + - '**/*.md' pull_request: branches: [master] paths-ignore: - 'docs/**' + - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_number || github.ref }} diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index e5c4bf7e68de..17e61de5b3a6 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -6,6 +6,7 @@ on: - master paths-ignore: - 'docs/**' + - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_number || github.ref }} diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 7bd22edba3c8..ae86097eb822 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -5,10 +5,12 @@ on: branches: [master] paths-ignore: - 'docs/**' + - '**/*.md' pull_request: branches: [master] paths-ignore: - 'docs/**' + - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_number || github.ref }} diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index fa8527c19a32..230233de3eb2 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -5,6 +5,7 @@ on: branches: [master] paths-ignore: - 'docs/**' + - '**/*.md' jobs: lint: diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index 932ef18d5143..adf2d0ddfe53 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -6,11 +6,13 @@ on: - master paths-ignore: - 'docs/**' + - '**/*.md' pull_request: branches: - master paths-ignore: - 'docs/**' + - '**/*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_number || github.ref }} From 5c27f0b3d488f1959374aed4eeac07bace6eb83e Mon Sep 17 00:00:00 2001 From: Bisakh Date: Mon, 25 Oct 2021 12:13:38 +0530 Subject: [PATCH 034/260] ci: test framework requires grpcurl in centos7 ci (#5316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- ci/centos7-ci.sh | 3 +++ ci/common.sh | 7 +++++++ ci/linux_openresty_common_runner.sh | 7 ++----- docs/en/latest/grpc-proxy.md | 2 ++ docs/zh/latest/grpc-proxy.md | 2 ++ t/grpc-proxy-test.sh | 10 +++++----- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index 9b6a48d63dee..4b39a104cb9e 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -59,6 +59,9 @@ install_dependencies() { # wait for grpc_server_example to fully start sleep 3 + # installing grpcurl + install_grpcurl + # install dependencies git clone https://github.com/iresty/test-nginx.git test-nginx make deps diff --git a/ci/common.sh b/ci/common.sh index bf06c49f1bd8..0aae0703c49f 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -31,4 +31,11 @@ create_lua_deps() { # luarocks install luacov-coveralls --tree=deps --local > build.log 2>&1 || (cat build.log && exit 1) } +install_grpcurl () { + # For more versions, visit https://github.com/fullstorydev/grpcurl/releases + GRPCURL_VERSION="1.8.5" + wget https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL_VERSION}/grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz + tar -xvf grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz -C /usr/local/bin +} + GRPC_SERVER_EXAMPLE_VER=20210819 diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index 0e6e9ff9a565..7649f6a9a9cc 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -57,11 +57,8 @@ do_install() { touch build-cache/grpc_server_example_"$GRPC_SERVER_EXAMPLE_VER" fi - if [ ! -f "build-cache/grpcurl" ]; then - wget https://github.com/api7/grpcurl/releases/download/20200314/grpcurl-amd64.tar.gz - tar -xvf grpcurl-amd64.tar.gz - mv grpcurl build-cache/ - fi + # installing grpcurl + install_grpcurl } script() { diff --git a/docs/en/latest/grpc-proxy.md b/docs/en/latest/grpc-proxy.md index dd71f03249df..3ec23bb844e2 100644 --- a/docs/en/latest/grpc-proxy.md +++ b/docs/en/latest/grpc-proxy.md @@ -66,6 +66,8 @@ $ grpcurl -insecure -import-path /pathtoprotos -proto helloworld.proto -d '{"n } ``` +> grpcurl is a CLI tool, similar to curl, that acts as a gRPC client and lets you interact with a gRPC server. For installation, please check out the official [documentation](https://github.com/fullstorydev/grpcurl#installation). + This means that the proxying is working. #### testing HTTP/2 with plaintext diff --git a/docs/zh/latest/grpc-proxy.md b/docs/zh/latest/grpc-proxy.md index e57a652d0714..ebd016550a1d 100644 --- a/docs/zh/latest/grpc-proxy.md +++ b/docs/zh/latest/grpc-proxy.md @@ -66,6 +66,8 @@ grpcurl -insecure -import-path /pathtoprotos -proto helloworld.proto \ } ``` +> grpcurl 是一个 CLI 工具,类似于 curl,充当 gRPC 客户端并让您与 gRPC 服务器进行交互。安装方式请查看官方[文档](https://github.com/fullstorydev/grpcurl#installation) + 这表示已成功代理。 ### 测试纯文本的 HTTP/2 diff --git a/t/grpc-proxy-test.sh b/t/grpc-proxy-test.sh index b6574cd6a8b1..c5e6584793d1 100755 --- a/t/grpc-proxy-test.sh +++ b/t/grpc-proxy-test.sh @@ -57,10 +57,10 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 }' # test grpc proxy with plaintext -./build-cache/grpcurl -plaintext -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' 127.0.0.1:9081 helloworld.Greeter.SayHello | grep 'Hello apisix' +grpcurl -plaintext -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' 127.0.0.1:9081 helloworld.Greeter.SayHello | grep 'Hello apisix' # test grpc proxy with ssl -./build-cache/grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' +grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' # the old way curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -76,7 +76,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 } }' -./build-cache/grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' +grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' #test grpcs proxy curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -92,7 +92,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 } }' -./build-cache/grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' +grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' if ! openresty -V 2>&1 | grep "apisix-nginx-module"; then echo "skip vanilla OpenResty" @@ -118,5 +118,5 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 } }' -./build-cache/grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' +grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' clean_up From b1e3a78360559a6cc83e3fdc5e3ec59440506fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 25 Oct 2021 22:00:45 +0800 Subject: [PATCH 035/260] feat: release 2.10.1 (#5325) --- CHANGELOG.md | 13 +++++ apisix/core/version.lua | 2 +- docs/en/latest/config.json | 2 +- docs/en/latest/how-to-build.md | 14 ++--- docs/zh/latest/CHANGELOG.md | 13 +++++ docs/zh/latest/config.json | 2 +- docs/zh/latest/how-to-build.md | 14 ++--- rockspec/apisix-2.10.1-0.rockspec | 95 +++++++++++++++++++++++++++++++ 8 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 rockspec/apisix-2.10.1-0.rockspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cb40fc80f29..790a4b19e437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ title: Changelog ## Table of Contents +- [2.10.1](#2101) - [2.10.0](#2100) - [2.9.0](#290) - [2.8.0](#280) @@ -46,6 +47,18 @@ title: Changelog - [0.7.0](#070) - [0.6.0](#060) +## 2.10.1 + +### Bugfix + +- fix(zipkin): response_span doesn't have correct start time [#5295](https://github.com/apache/apisix/pull/5295) +- fix(ext-plugin): don't use stale key [#5309](https://github.com/apache/apisix/pull/5309) +- fix: route's timeout should not be overwrittern by service [#5219](https://github.com/apache/apisix/pull/5219) +- fix: filter nil plugin conf triggered by etcd dir init [#5204](https://github.com/apache/apisix/pull/5204) +- fix: pass correct host header to health checker target nodes [#5175](https://github.com/apache/apisix/pull/5175) +- fix: upgrade lua-resty-balancer to 0.04 [#5144](https://github.com/apache/apisix/pull/5144) +- fix(prometheus): avoid negative latency caused by inconsistent Nginx metrics [#5150](https://github.com/apache/apisix/pull/5150) + ## 2.10.0 ### Change diff --git a/apisix/core/version.lua b/apisix/core/version.lua index 6217100859a7..350f101bf90b 100644 --- a/apisix/core/version.lua +++ b/apisix/core/version.lua @@ -15,5 +15,5 @@ -- limitations under the License. -- return { - VERSION = "2.10.0" + VERSION = "2.10.1" } diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index deabb64d9e26..7c88b9c3af7d 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -1,5 +1,5 @@ { - "version": "2.10.0", + "version": "2.10.1", "sidebar": [ { "type": "category", diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 799c47bedd95..8a2d898bffb9 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -56,7 +56,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep This installation method is suitable for CentOS 7, please run the following command to install Apache APISIX. ```shell -sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.0/apisix-2.10.0-0.el7.x86_64.rpm +sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.1/apisix-2.10.1-0.el7.x86_64.rpm ``` > You can also install the RPM package via running `sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.0-0.el7.x86_64.rpm`. @@ -71,16 +71,16 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a ### Installation via Source Release Package -1. Create a directory named `apisix-2.10.0`. +1. Create a directory named `apisix-2.10.1`. ```shell - mkdir apisix-2.10.0 + mkdir apisix-2.10.1 ``` 2. Download Apache APISIX Release source package. ```shell - wget https://downloads.apache.org/apisix/2.10.0/apache-apisix-2.10.0-src.tgz + wget https://downloads.apache.org/apisix/2.10.1/apache-apisix-2.10.1-src.tgz ``` You can also download the Apache APISIX Release source package from the Apache APISIX website. The [Apache APISIX Official Website - Download Page](https://apisix.apache.org/downloads/) also provides source packages for Apache APISIX, APISIX Dashboard and APISIX Ingress Controller. @@ -88,14 +88,14 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a 3. Unzip the Apache APISIX Release source package. ```shell - tar zxvf apache-apisix-2.10.0-src.tgz -C apisix-2.10.0 + tar zxvf apache-apisix-2.10.1-src.tgz -C apisix-2.10.1 ``` 4. Install the runtime dependent Lua libraries. ```shell - # Switch to the apisix-2.10.0 directory - cd apisix-2.10.0 + # Switch to the apisix-2.10.1 directory + cd apisix-2.10.1 # Create dependencies make deps # Install apisix command diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index 8546b34f41ec..4d683c8f8c4f 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -23,6 +23,7 @@ title: CHANGELOG ## Table of Contents +- [2.10.1](#2101) - [2.10.0](#2100) - [2.9.0](#290) - [2.8.0](#280) @@ -46,6 +47,18 @@ title: CHANGELOG - [0.7.0](#070) - [0.6.0](#060) +## 2.10.1 + +### Bugfix + +- 更正 zipkin 插件 response_span 的开始时间 [#5295](https://github.com/apache/apisix/pull/5295) +- 避免发送过期 key 给 plugin runner [#5309](https://github.com/apache/apisix/pull/5309) +- 更正 route 的 timeout 被 service 覆盖的问题 [#5219](https://github.com/apache/apisix/pull/5219) +- 过滤掉初始化 etcd 数据时产生的空 plugin conf [#5204](https://github.com/apache/apisix/pull/5204) +- 健康检查特定情况下会发送错误的 Host header [#5175](https://github.com/apache/apisix/pull/5175) +- 升级 lua-resty-balancer 到 0.04 [#5144](https://github.com/apache/apisix/pull/5144) +- prometheus 插件修复偶发的 latency 为负数的问题 [#5150](https://github.com/apache/apisix/pull/5150) + ## 2.10.0 ### Change diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 5e8f25ad1003..d225a6364109 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -1,5 +1,5 @@ { - "version": "2.10.0", + "version": "2.10.1", "sidebar": [ { "type": "category", diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index cebac9ecbb29..cfcfa6f97ca4 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -56,7 +56,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep 这种安装方式适用于 CentOS 7 操作系统,请运行以下命令安装 Apache APISIX。 ```shell -sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.0/apisix-2.10.0-0.el7.x86_64.rpm +sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.1/apisix-2.10.1-0.el7.x86_64.rpm ``` > 您也可以运行 `sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.0-0.el7.x86_64.rpm` 命令安装。 @@ -71,16 +71,16 @@ sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.0/ap ### 通过源码包安装 -1. 创建一个名为 `apisix-2.10.0` 的目录。 +1. 创建一个名为 `apisix-2.10.1` 的目录。 ```shell - mkdir apisix-2.10.0 + mkdir apisix-2.10.1 ``` 2. 下载 Apache APISIX Release 源码包: ```shell - wget https://downloads.apache.org/apisix/2.10.0/apache-apisix-2.10.0-src.tgz + wget https://downloads.apache.org/apisix/2.10.1/apache-apisix-2.10.1-src.tgz ``` 您也可以通过 Apache APISIX 官网下载 Apache APISIX Release 源码包。 Apache APISIX 官网也提供了 Apache APISIX、APISIX Dashboard 和 APISIX Ingress Controller 的源码包,详情请参考[Apache APISIX 官网-下载页](https://apisix.apache.org/zh/downloads)。 @@ -88,14 +88,14 @@ sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.0/ap 3. 解压 Apache APISIX Release 源码包: ```shell - tar zxvf apache-apisix-2.10.0-src.tgz -C apisix-2.10.0 + tar zxvf apache-apisix-2.10.1-src.tgz -C apisix-2.10.1 ``` 4. 安装运行时依赖的 Lua 库: ```shell - # 切换到 apisix-2.10.0 目录 - cd apisix-2.10.0 + # 切换到 apisix-2.10.1 目录 + cd apisix-2.10.1 # 创建依赖 make deps # 安装 apisix 命令 diff --git a/rockspec/apisix-2.10.1-0.rockspec b/rockspec/apisix-2.10.1-0.rockspec new file mode 100644 index 000000000000..962770a8db6c --- /dev/null +++ b/rockspec/apisix-2.10.1-0.rockspec @@ -0,0 +1,95 @@ +-- +-- 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. +-- + +package = "apisix" +version = "2.10.1-0" +supported_platforms = {"linux", "macosx"} + +source = { + url = "git://github.com/apache/apisix", + branch = "2.10.1", +} + +description = { + summary = "Apache APISIX is a cloud-native microservices API gateway, delivering the ultimate performance, security, open source and scalable platform for all your APIs and microservices.", + homepage = "https://github.com/apache/apisix", + license = "Apache License 2.0", +} + +dependencies = { + "lua-resty-ctxdump = 0.1-0", + "lua-resty-dns-client = 5.2.0", + "lua-resty-template = 2.0", + "lua-resty-etcd = 1.5.4", + "api7-lua-resty-http = 0.2.0", + "lua-resty-balancer = 0.04", + "lua-resty-ngxvar = 0.5.2", + "lua-resty-jit-uuid = 0.0.7", + "lua-resty-healthcheck-api7 = 2.2.0", + "lua-resty-jwt = 0.2.0", + "lua-resty-hmac-ffi = 0.05", + "lua-resty-cookie = 0.1.0", + "lua-resty-session = 2.24", + "opentracing-openresty = 0.1", + "lua-resty-radixtree = 2.8.1", + "lua-protobuf = 0.3.3", + "lua-resty-openidc = 1.7.2-1", + "luafilesystem = 1.7.0-2", + "api7-lua-tinyyaml = 0.3.0", + "nginx-lua-prometheus = 0.20210206", + "jsonschema = 0.9.5", + "lua-resty-ipmatcher = 0.6.1", + "lua-resty-kafka = 0.07", + "lua-resty-logger-socket = 2.0-0", + "skywalking-nginx-lua = 0.4-1", + "base64 = 1.5-2", + "binaryheap = 0.4", + "dkjson = 2.5-2", + "resty-redis-cluster = 1.02-4", + "lua-resty-expr = 1.3.1", + "graphql = 0.0.2", + "argparse = 0.7.1-1", + "luasocket = 3.0rc1-2", + "luasec = 0.9-1", + "lua-resty-consul = 0.3-2", + "penlight = 1.9.2-1", + "ext-plugin-proto = 0.3.0", + "casbin = 1.26.0", + "api7-snowflake = 2.0-1", + "inspect == 3.1.1", +} + +build = { + type = "make", + build_variables = { + CFLAGS="$(CFLAGS)", + LIBFLAG="$(LIBFLAG)", + LUA_LIBDIR="$(LUA_LIBDIR)", + LUA_BINDIR="$(LUA_BINDIR)", + LUA_INCDIR="$(LUA_INCDIR)", + LUA="$(LUA)", + OPENSSL_INCDIR="$(OPENSSL_INCDIR)", + OPENSSL_LIBDIR="$(OPENSSL_LIBDIR)", + }, + install_variables = { + INST_PREFIX="$(PREFIX)", + INST_BINDIR="$(BINDIR)", + INST_LIBDIR="$(LIBDIR)", + INST_LUADIR="$(LUADIR)", + INST_CONFDIR="$(CONFDIR)", + }, +} From 60f14bb1e361a85849827ca9e616080fcec05157 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Tue, 26 Oct 2021 06:35:11 +0530 Subject: [PATCH 036/260] refactor: migrate grpc proxy tests from shell to test Nginx (#5299) --- t/node/grpc-proxy-mtls.t | 86 ++++++++++++++++ t/node/grpc-proxy-unary.t | 209 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 t/node/grpc-proxy-mtls.t create mode 100644 t/node/grpc-proxy-unary.t diff --git a/t/node/grpc-proxy-mtls.t b/t/node/grpc-proxy-mtls.t new file mode 100644 index 000000000000..472acf1782d8 --- /dev/null +++ b/t/node/grpc-proxy-mtls.t @@ -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. +# +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'); +} + +no_long_string(); +no_root_location(); +no_shuffle(); +add_block_preprocessor(sub { + my ($block) = @_; + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: Unary API grpcs proxy test with mTLS +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHello + methods: [ + POST + ] + upstream: + scheme: grpcs + tls: + client_cert: "-----BEGIN CERTIFICATE-----\nMIIDOjCCAiICAwD6zzANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJjbjESMBAG\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDTALBgNVBAoMBGFwaTcx\nDDAKBgNVBAsMA29wczEWMBQGA1UEAwwNY2EuYXBpc2l4LmRldjAeFw0yMDA2MjAx\nMzE1MDBaFw0zMDA3MDgxMzE1MDBaMF0xCzAJBgNVBAYTAmNuMRIwEAYDVQQIDAlH\ndWFuZ0RvbmcxDTALBgNVBAoMBGFwaTcxDzANBgNVBAcMBlpodUhhaTEaMBgGA1UE\nAwwRY2xpZW50LmFwaXNpeC5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQCfKI8uiEH/ifZikSnRa3/E2B4ohVWRwjo/IxyDEWomgR4tLk1pSJhP/4SC\nLWuMQTFWTbSqt1IFYy4ZbVSHHyGoNPmJGrHRJCGE+sgpfzn0GjV4lXQPJD0k6GR1\nCX2Mo1TWdFqSJ/Hc5AQwcQFnPfoLAwsBy4yqrlmf96ZAUytl/7Zkjf4P7mJkJHtM\n/WgSR0pGhjZTAGRf5DJWoO51ki3i3JI+15mOhmnnCpnksnGVPfl92q92Hz/4v3iq\nE+UThPYRpcGbnddzMvPaCXiavg8B/u2LVbn4l0adamqQGepOAjD/1xraOVP2W22W\n0PztDXJ4rLe+capNS4oGuSUfkIENAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHKn\nHxUhuk/nL2Sg5UB84OoJe5XPgNBvVMKN0c/NAPKVIPninvUcG/mHeKexPzE0sMga\nRNos75N2199EXydqUcsJ8jL0cNtQ2k5JQXXg0ntNC4tuCgIKAOnO879y5hSG36e5\n7wmAoVKnabgjej09zG1kkXvAmpgqoxeVCu7h7fK+AurLbsGCTaHoA5pG1tcHDxJQ\nfpVcbBfwQDSBW3SQjiRqX453/01nw6kbOeLKYraJysaG8ZU2K8+WpW6JDubciHjw\nfQnpU2U16XKivhxeuKYrV/INL0sxj/fZraNYErvJWzh5llvIdNLmeSPmvb50JUIs\n+lDqn1MobTXzDpuCFXA=\n-----END CERTIFICATE-----\n", + client_key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAnyiPLohB/4n2YpEp0Wt/xNgeKIVVkcI6PyMcgxFqJoEeLS5N\naUiYT/+Egi1rjEExVk20qrdSBWMuGW1Uhx8hqDT5iRqx0SQhhPrIKX859Bo1eJV0\nDyQ9JOhkdQl9jKNU1nRakifx3OQEMHEBZz36CwMLAcuMqq5Zn/emQFMrZf+2ZI3+\nD+5iZCR7TP1oEkdKRoY2UwBkX+QyVqDudZIt4tySPteZjoZp5wqZ5LJxlT35fdqv\ndh8/+L94qhPlE4T2EaXBm53XczLz2gl4mr4PAf7ti1W5+JdGnWpqkBnqTgIw/9ca\n2jlT9lttltD87Q1yeKy3vnGqTUuKBrklH5CBDQIDAQABAoIBAHDe5bPdQ9jCcW3z\nfpGax/DER5b6//UvpfkSoGy/E+Wcmdb2yEVLC2FoVwOuzF+Z+DA5SU/sVAmoDZBQ\nvapZxJeygejeeo5ULkVNSFhNdr8LOzJ54uW+EHK1MFDj2xq61jaEK5sNIvRA7Eui\nSJl8FXBrxwmN3gNJRBwzF770fImHUfZt0YU3rWKw5Qin7QnlUzW2KPUltnSEq/xB\nkIzyWpuj7iAm9wTjH9Vy06sWCmxj1lzTTXlanjPb1jOTaOhbQMpyaAzRgQN8PZiE\nYKCarzVj7BJr7/vZYpnQtQDY12UL5n33BEqMP0VNHVqv+ZO3bktfvlwBru5ZJ7Cf\nURLsSc0CgYEAyz7FzV7cZYgjfUFD67MIS1HtVk7SX0UiYCsrGy8zA19tkhe3XVpc\nCZSwkjzjdEk0zEwiNAtawrDlR1m2kverbhhCHqXUOHwEpujMBjeJCNUVEh3OABr8\nvf2WJ6D1IRh8FA5CYLZP7aZ41fcxAnvIPAEThemLQL3C4H5H5NG2WFsCgYEAyHhP\nonpS/Eo/OXKYFLR/mvjizRVSomz1lVVL+GWMUYQsmgsPyBJgyAOX3Pqt9catgxhM\nDbEr7EWTxth3YeVzamiJPNVK0HvCax9gQ0KkOmtbrfN54zBHOJ+ieYhsieZLMgjx\niu7Ieo6LDGV39HkvekzutZpypiCpKlMaFlCFiLcCgYEAmAgRsEj4Nh665VPvuZzH\nZIgZMAlwBgHR7/v6l7AbybcVYEXLTNJtrGEEH6/aOL8V9ogwwZuIvb/TEidCkfcf\nzg/pTcGf2My0MiJLk47xO6EgzNdso9mMG5ZYPraBBsuo7NupvWxCp7NyCiOJDqGH\nK5NmhjInjzsjTghIQRq5+qcCgYEAxnm/NjjvslL8F69p/I3cDJ2/RpaG0sMXvbrO\nVWaMryQyWGz9OfNgGIbeMu2Jj90dar6ChcfUmb8lGOi2AZl/VGmc/jqaMKFnElHl\nJ5JyMFicUzPMiG8DBH+gB71W4Iy+BBKwugHBQP2hkytewQ++PtKuP+RjADEz6vCN\n0mv0WS8CgYBnbMRP8wIOLJPRMw/iL9BdMf606X4xbmNn9HWVp2mH9D3D51kDFvls\n7y2vEaYkFv3XoYgVN9ZHDUbM/YTUozKjcAcvz0syLQb8wRwKeo+XSmo09+360r18\nzRugoE7bPl39WdGWaW3td0qf1r9z3sE2iWUTJPRQ3DYpsLOYIgyKmw==\n-----END RSA PRIVATE KEY-----\n" + nodes: + "127.0.0.1:50053": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} diff --git a/t/node/grpc-proxy-unary.t b/t/node/grpc-proxy-unary.t new file mode 100644 index 000000000000..5397179dc6df --- /dev/null +++ b/t/node/grpc-proxy-unary.t @@ -0,0 +1,209 @@ +# +# 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'; + +no_long_string(); +no_root_location(); +no_shuffle(); +add_block_preprocessor(sub { + my ($block) = @_; + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: Unary API gRPC proxy +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHello + methods: [ + POST + ] + upstream: + scheme: grpc + nodes: + "127.0.0.1:50051": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local opts= { + merge_stderr = true, + } + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello", opts) + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} + + + +=== TEST 2: Unary API gRPC proxy test [the old way] +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHello + methods: [ + POST + ] + service_protocol: grpc + upstream: + nodes: + "127.0.0.1:50051": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} + + + +=== TEST 3: Unary API grpcs proxy test +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHello + methods: [ + POST + ] + upstream: + scheme: grpcs + nodes: + "127.0.0.1:50052": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} + + + +=== TEST 4: Unary API gRPC proxy with tls +--- http2 +--- apisix_yaml +ssl: + - + id: 1 + cert: "-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n" + key: "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n" + sni: test.com +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHello + methods: [ + POST + ] + upstream: + scheme: grpc + nodes: + "127.0.0.1:50051": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -insecure -d '{\"name\":\"apisix\"}' test.com:1994 helloworld.Greeter.SayHello") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} From 8b8f55c17fc1538b830b7bf3dfb11c16b4383787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 26 Oct 2021 20:47:05 +0800 Subject: [PATCH 037/260] docs: use mirror when installing dependencies in China (#5332) --- docs/zh/latest/how-to-build.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index cfcfa6f97ca4..bfbcd83675e9 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -96,8 +96,8 @@ sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.1/ap ```shell # 切换到 apisix-2.10.1 目录 cd apisix-2.10.1 - # 创建依赖 - make deps + # 安装依赖 + LUAROCKS_SERVER=https://luarocks.cn make deps # 安装 apisix 命令 make install ``` From a08e02c370a982a077851678c46752bb2096c00c Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Wed, 27 Oct 2021 08:57:33 +0800 Subject: [PATCH 038/260] ci: set timeout for github actions (#5335) --- .github/workflows/build.yml | 1 + .github/workflows/centos7-ci.yml | 1 + .github/workflows/chaos.yml | 1 + .github/workflows/cli.yml | 1 + .github/workflows/code-lint.yml | 1 + .github/workflows/doc-lint.yml | 1 + .github/workflows/fuzzing-ci.yaml | 1 + .github/workflows/license-checker.yml | 1 + .github/workflows/lint.yml | 1 + .github/workflows/stale.yml | 3 +++ 10 files changed, 12 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16453f34e1ed..c1551ed91f44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,7 @@ jobs: - linux_tengine runs-on: ${{ matrix.platform }} + timeout-minutes: 90 env: SERVER_NAME: ${{ matrix.os_name }} OPENRESTY_VERSION: default diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 2f4ba8c5a560..899a33ceb896 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -20,6 +20,7 @@ jobs: test_apisix: name: run ci on centos7 runs-on: ubuntu-latest + timeout-minutes: 90 steps: - name: Check out code diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 17e61de5b3a6..13cbc3ce698f 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -15,6 +15,7 @@ concurrency: jobs: chaos-test: runs-on: ubuntu-latest + timeout-minutes: 35 steps: - uses: actions/checkout@v2.3.5 with: diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index ae86097eb822..e5899db1b723 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -29,6 +29,7 @@ jobs: - linux_apisix_current_luarocks_in_customed_nginx runs-on: ${{ matrix.platform }} + timeout-minutes: 15 env: SERVER_NAME: ${{ matrix.job_name }} OPENRESTY_VERSION: default diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index 230233de3eb2..7cd660afd3f9 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -10,6 +10,7 @@ on: jobs: lint: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v2.3.5 - name: Install diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 4e4428afa694..8f7e58f6d7bb 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -10,6 +10,7 @@ jobs: markdownlint: name: 🍇 Markdown runs-on: ubuntu-latest + timeout-minutes: 1 steps: - uses: actions/checkout@v2.3.5 - name: 🚀 Use Node.js diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index adf2d0ddfe53..00edfd09201c 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -22,6 +22,7 @@ jobs: test_apisix: name: run fuzzing runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Check out code diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index 00d1363d9cac..ad2e71c7f052 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -29,6 +29,7 @@ on: jobs: check-license: runs-on: ubuntu-latest + timeout-minutes: 3 steps: - uses: actions/checkout@v2.3.5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 337bd1e05c98..f4ed245e6416 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,7 @@ jobs: trailing-whitespace: name: 🌌 Trailing whitespace runs-on: ubuntu-latest + timeout-minutes: 1 steps: - uses: actions/checkout@v2.3.5 - name: 🧹 Check for trailing whitespace diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4c7d3affd350..984fb8c047b7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,3 +1,5 @@ +name: Stable Test + on: workflow_dispatch: schedule: @@ -7,6 +9,7 @@ jobs: prune_stale: name: Prune Stale runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Prune Stale From 5fd95ce608973607e72f8be601bc418fe8aad2a8 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 27 Oct 2021 07:39:39 +0530 Subject: [PATCH 039/260] perf(grpc transcode): find proto in O(1) way (#5331) --- apisix/plugins/grpc-transcode/proto.lua | 33 ++++++++++++++++----- apisix/plugins/grpc-transcode/util.lua | 39 ++++++++++--------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/apisix/plugins/grpc-transcode/proto.lua b/apisix/plugins/grpc-transcode/proto.lua index ec515c740917..33d84ebeccce 100644 --- a/apisix/plugins/grpc-transcode/proto.lua +++ b/apisix/plugins/grpc-transcode/proto.lua @@ -18,6 +18,7 @@ local core = require("apisix.core") local config_util = require("apisix.core.config_util") local protoc = require("protoc") local pcall = pcall +local ipairs = ipairs local protos @@ -25,6 +26,7 @@ local lrucache_proto = core.lrucache.new({ ttl = 300, count = 100 }) +local proto_fake_file = "filename for loaded" local function compile_proto(content) local _p = protoc.new() @@ -33,7 +35,7 @@ local function compile_proto(content) -- name to keep the code below unchanged, or we can create our own load function with returning -- the loaded DescriptorProto table additionally, see more details in -- https://github.com/apache/apisix/pull/4368 - local ok, res = pcall(_p.load, _p, content, "filename for loaded") + local ok, res = pcall(_p.load, _p, content, proto_fake_file) if not ok then return nil, res end @@ -46,6 +48,12 @@ local function compile_proto(content) end +local _M = { + version = 0.1, + compile_proto = compile_proto, + proto_fake_file = proto_fake_file +} + local function create_proto_obj(proto_id) if protos.values == nil then return nil @@ -63,14 +71,25 @@ local function create_proto_obj(proto_id) return nil, "failed to find proto by id: " .. proto_id end - return compile_proto(content) -end + local compiled, err = compile_proto(content) + if not compiled then + return nil, err + end -local _M = { - version = 0.1, - compile_proto = compile_proto, -} + local index = {} + for _, s in ipairs(compiled[proto_fake_file].service or {}) do + local method_index = {} + for _, m in ipairs(s.method) do + method_index[m.name] = m + end + + index[compiled[proto_fake_file].package .. '.' .. s.name] = method_index + end + + compiled[proto_fake_file].index = index + return compiled +end function _M.fetch(proto_id) diff --git a/apisix/plugins/grpc-transcode/util.lua b/apisix/plugins/grpc-transcode/util.lua index 102e50d4171d..8fbe96283199 100644 --- a/apisix/plugins/grpc-transcode/util.lua +++ b/apisix/plugins/grpc-transcode/util.lua @@ -14,37 +14,30 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- -local core = require("apisix.core") -local json = core.json -local pb = require("pb") -local ngx = ngx -local pairs = pairs -local ipairs = ipairs -local string = string -local tonumber = tonumber -local type = type +local core = require("apisix.core") +local proto_fake_file = require("apisix.plugins.grpc-transcode.proto").proto_fake_file +local json = core.json +local pb = require("pb") +local ngx = ngx +local string = string +local tonumber = tonumber +local type = type local _M = {version = 0.1} function _M.find_method(protos, service, method) - for k, loaded in pairs(protos) do - if type(loaded) == 'table' then - local package = loaded.package - for _, s in ipairs(loaded.service or {}) do - if package .. "." .. s.name == service then - for _, m in ipairs(s.method) do - if m.name == method then - return m - end - end - end - end - end + local loaded = protos[proto_fake_file] + if not loaded or type(loaded) ~= "table" then + return nil + end + + if not loaded.index[service] or type(loaded.index[service]) ~= "table" then + return nil end - return nil + return loaded.index[service][method] end From 9df6e887349e31e9159f99dc7a874dba3798a299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 27 Oct 2021 10:27:27 +0800 Subject: [PATCH 040/260] test(wasm): add fault-injection example (#5337) --- t/wasm/fault-injection.t | 186 +++++++++++++++++++++++++++++++++ t/wasm/fault-injection/main.go | 108 +++++++++++++++++++ t/wasm/go.mod | 6 +- t/wasm/go.sum | 7 ++ 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 t/wasm/fault-injection.t create mode 100644 t/wasm/fault-injection/main.go diff --git a/t/wasm/fault-injection.t b/t/wasm/fault-injection.t new file mode 100644 index 000000000000..addedddf64c2 --- /dev/null +++ b/t/wasm/fault-injection.t @@ -0,0 +1,186 @@ +# +# 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; + +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'); +} + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $extra_yaml_config = <<_EOC_; +wasm: + plugins: + - name: wasm_fault_injection + priority: 7997 + file: t/wasm/fault-injection/main.go.wasm +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: fault injection +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_fault_injection": { + "conf": "{\"http_status\":401, \"body\":\"HIT\n\"}" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit +--- request +GET /hello +--- error_code: 401 +--- response_body +HIT + + + +=== TEST 3: fault injection, with 0 percentage +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_fault_injection": { + "conf": "{\"http_status\":401, \"percentage\":0}" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: hit +--- request +GET /hello +--- response_body +hello world + + + +=== TEST 5: fault injection without body +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm_fault_injection": { + "conf": "{\"http_status\":401}" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 6: hit +--- request +GET /hello +--- error_code: 401 +--- response_body_like eval +qr/401 Authorization Required<\/title>/ diff --git a/t/wasm/fault-injection/main.go b/t/wasm/fault-injection/main.go new file mode 100644 index 000000000000..698bb97ac7dc --- /dev/null +++ b/t/wasm/fault-injection/main.go @@ -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. + */ + +package main + +import ( + "math/rand" + + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" + + // tinygo doesn't support encoding/json, see https://github.com/tinygo-org/tinygo/issues/447 + "github.com/valyala/fastjson" +) + +func main() { + proxywasm.SetVMContext(&vmContext{}) +} + +type vmContext struct { + types.DefaultVMContext +} + +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +type pluginContext struct { + types.DefaultPluginContext + Body []byte + HttpStatus uint32 + Percentage int +} + +func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + data, err := proxywasm.GetPluginConfiguration() + if err != nil { + proxywasm.LogErrorf("error reading plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + var p fastjson.Parser + v, err := p.ParseBytes(data) + if err != nil { + proxywasm.LogErrorf("erorr decoding plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + ctx.Body = v.GetStringBytes("body") + ctx.HttpStatus = uint32(v.GetUint("http_status")) + if v.Exists("percentage") { + ctx.Percentage = v.GetInt("percentage") + } else { + ctx.Percentage = 100 + } + + // schema check + if ctx.HttpStatus < 200 { + proxywasm.LogError("bad http_status") + return types.OnPluginStartStatusFailed + } + if ctx.Percentage < 0 || ctx.Percentage > 100 { + proxywasm.LogError("bad percentage") + return types.OnPluginStartStatusFailed + } + + return types.OnPluginStartStatusOK +} + +func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpLifecycle{parent: ctx} +} + +type httpLifecycle struct { + types.DefaultHttpContext + parent *pluginContext +} + +func sampleHit(percentage int) bool { + return rand.Intn(100) < percentage +} + +func (ctx *httpLifecycle) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + plugin := ctx.parent + if !sampleHit(plugin.Percentage) { + return types.ActionContinue + } + + err := proxywasm.SendHttpResponse(plugin.HttpStatus, nil, plugin.Body, -1) + if err != nil { + proxywasm.LogErrorf("failed to send local response: %v", err) + return types.ActionContinue + } + return types.ActionPause +} diff --git a/t/wasm/go.mod b/t/wasm/go.mod index 9a875c8144c9..3f9de8af9c55 100644 --- a/t/wasm/go.mod +++ b/t/wasm/go.mod @@ -2,5 +2,9 @@ module github.com/api7/wasm-nginx-module go 1.15 -require github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31 +require ( + github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31 + github.com/valyala/fastjson v1.6.3 +) + //replace github.com/tetratelabs/proxy-wasm-go-sdk => ../proxy-wasm-go-sdk diff --git a/t/wasm/go.sum b/t/wasm/go.sum index 599f22615046..97ddff755a28 100644 --- a/t/wasm/go.sum +++ b/t/wasm/go.sum @@ -1,8 +1,15 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31 h1:V3GXN5nayOdIU3NypbxVegGFCVGm78qOA8Q7wkeudy8= github.com/tetratelabs/proxy-wasm-go-sdk v0.14.1-0.20210819090022-1e4e69881a31/go.mod h1:qZ+4i6e2wHlhnhgpH0VG4QFzqd2BEvQbQFU0npt2e2k= +github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= +github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 83651715a613280c4e4d706d57670c1a160bafed Mon Sep 17 00:00:00 2001 From: Bisakh <bisakhmondal00@gmail.com> Date: Thu, 28 Oct 2021 06:19:08 +0530 Subject: [PATCH 041/260] test: grpc stream proxy test with test nginx (#5319) Co-authored-by: leslie <59061168+leslie-tsang@users.noreply.github.com> --- .github/workflows/build.yml | 5 + .gitmodules | 3 + ci/centos7-ci.sh | 26 ++-- ci/linux_openresty_common_runner.sh | 68 +++------- ci/linux_tengine_runner.sh | 2 +- t/grpc-proxy-test.sh | 122 ------------------ t/grpc_server_example | 1 + t/node/grpc-proxy-mtls.t | 50 +++++++- t/node/grpc-proxy-stream.t | 185 ++++++++++++++++++++++++++++ t/node/grpc-proxy-unary.t | 8 +- 10 files changed, 283 insertions(+), 187 deletions(-) delete mode 100755 t/grpc-proxy-test.sh create mode 160000 t/grpc_server_example create mode 100644 t/node/grpc-proxy-stream.t diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1551ed91f44..e1dadc1b5372 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,11 @@ jobs: with: submodules: recursive + - name: Setup Go + uses: actions/setup-go@v2.1.4 + with: + go-version: "1.15" + - name: Cache deps uses: actions/cache@v2.1.6 env: diff --git a/.gitmodules b/.gitmodules index 78dcdd805b86..12854c44f59a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule ".github/actions/action-tmate"] path = .github/actions/action-tmate url = https://github.com/mxschmitt/action-tmate +[submodule "t/grpc_server_example"] + path = t/grpc_server_example + url = https://github.com/api7/grpc_server_example diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index 4b39a104cb9e..29c7c36d62f2 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -44,18 +44,28 @@ install_dependencies() { yum install -y cpanminus perl cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) - # install and start grpc_server_example + # add go1.15 binary to the path mkdir build-cache - wget https://github.com/api7/grpc_server_example/releases/download/"$GRPC_SERVER_EXAMPLE_VER"/grpc_server_example-amd64.tar.gz - tar -xvf grpc_server_example-amd64.tar.gz - mv grpc_server_example build-cache/ - git clone https://github.com/iresty/grpc_server_example.git grpc_server_example - cd grpc_server_example/ && mv proto/ ../build-cache/ && cd .. - ./build-cache/grpc_server_example \ + # centos-7 ci runs on a docker container with the centos image on top of ubuntu host. Go is required inside the container. + cd build-cache/ && wget https://golang.org/dl/go1.15.linux-amd64.tar.gz && tar -xf go1.15.linux-amd64.tar.gz + export PATH=$PATH:$(pwd)/go/bin + cd .. + # install and start grpc_server_example + cd t/grpc_server_example + + # unless pulled recursively, the submodule directory will remain empty. So it's better to initialize and set the submodule to the particular commit. + if [ ! "$(ls -A . )" ]; then + git submodule init + git submodule update + fi + + CGO_ENABLED=0 go build + ./grpc_server_example \ -grpc-address :50051 -grpcs-address :50052 -grpcs-mtls-address :50053 \ - -crt ./t/certs/apisix.crt -key ./t/certs/apisix.key -ca ./t/certs/mtls_ca.crt \ + -crt ../certs/apisix.crt -key ../certs/apisix.key -ca ../certs/mtls_ca.crt \ > grpc_server_example.log 2>&1 || (cat grpc_server_example.log && exit 1)& + cd ../../ # wait for grpc_server_example to fully start sleep 3 diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index 7649f6a9a9cc..7916d1f95bdc 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -44,18 +44,15 @@ do_install() { make utils mkdir -p build-cache - if [ ! -f "build-cache/grpc_server_example_$GRPC_SERVER_EXAMPLE_VER" ]; then - wget https://github.com/api7/grpc_server_example/releases/download/"$GRPC_SERVER_EXAMPLE_VER"/grpc_server_example-amd64.tar.gz - tar -xvf grpc_server_example-amd64.tar.gz - mv grpc_server_example build-cache/ + # install and start grpc_server_example + cd t/grpc_server_example - git clone --depth 1 https://github.com/api7/grpc_server_example.git grpc_server_example - pushd grpc_server_example/ || exit 1 - mv proto/ ../build-cache/ - popd || exit 1 - - touch build-cache/grpc_server_example_"$GRPC_SERVER_EXAMPLE_VER" + if [ ! "$(ls -A . )" ]; then # for local development only + git submodule init + git submodule update fi + CGO_ENABLED=0 go build + cd ../../ # installing grpcurl install_grpcurl @@ -67,51 +64,20 @@ script() { ./utils/set-dns.sh - ./build-cache/grpc_server_example \ + ./t/grpc_server_example/grpc_server_example \ -grpc-address :50051 -grpcs-address :50052 -grpcs-mtls-address :50053 \ -crt ./t/certs/apisix.crt -key ./t/certs/apisix.key -ca ./t/certs/mtls_ca.crt \ & - # listen 9081 for http2 with plaintext - echo ' -apisix: - node_listen: - - port: 9080 - enable_http2: false - - port: 9081 - enable_http2: true - ' > conf/config.yaml - - ./bin/apisix help - ./bin/apisix init - ./bin/apisix init_etcd - ./bin/apisix start - - #start again --> fail - res=`./bin/apisix start` - if ! echo "$res" | grep "APISIX is running"; then - echo "failed: APISIX runs repeatedly" - exit 1 - fi - - #kill apisix - sudo kill -9 `ps aux | grep apisix | grep nginx | awk '{print $2}'` - - #start -> ok - res=`./bin/apisix start` - if echo "$res" | grep "APISIX is running"; then - echo "failed: shouldn't stop APISIX running after kill the old process." - exit 1 - fi - - sleep 1 - cat logs/error.log - - ./t/grpc-proxy-test.sh - sleep 1 - - ./bin/apisix stop - sleep 1 + # ensure grpc server example is already started + for (( i = 0; i <= 100; i++ )); do + if [[ "$i" -eq 100 ]]; then + echo "failed to start grpc_server_example in time" + exit 1 + fi + nc -zv 127.0.0.1 50051 && break + sleep 1 + done # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t FLUSH_ETCD=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t diff --git a/ci/linux_tengine_runner.sh b/ci/linux_tengine_runner.sh index ec3d3b626e66..116c4f026c7f 100755 --- a/ci/linux_tengine_runner.sh +++ b/ci/linux_tengine_runner.sh @@ -217,7 +217,7 @@ script() { openresty -V - ./build-cache/grpc_server_example & + ./t/grpc_server_example/grpc_server_example & ./bin/apisix help ./bin/apisix init diff --git a/t/grpc-proxy-test.sh b/t/grpc-proxy-test.sh deleted file mode 100755 index c5e6584793d1..000000000000 --- a/t/grpc-proxy-test.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/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. -# - -clean_up() { - #delete test data - curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE - curl http://127.0.0.1:9080/apisix/admin/ssl/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X DELETE -} - -set -ex - -# ensure grpc server example is already started -for (( i = 0; i <= 100; i++ )); do - if [[ "$i" -eq 100 ]]; then - echo "failed to start grpc_server_example in time" - exit 1 - fi - nc -zv 127.0.0.1 50051 && break - sleep 1 -done - -#set ssl -curl http://127.0.0.1:9080/apisix/admin/ssl/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' -{ - "cert": "-----BEGIN CERTIFICATE-----\nMIIEojCCAwqgAwIBAgIJAK253pMhgCkxMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV\nBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxDzANBgNVBAcMBlpodUhhaTEPMA0G\nA1UECgwGaXJlc3R5MREwDwYDVQQDDAh0ZXN0LmNvbTAgFw0xOTA2MjQyMjE4MDVa\nGA8yMTE5MDUzMTIyMTgwNVowVjELMAkGA1UEBhMCQ04xEjAQBgNVBAgMCUd1YW5n\nRG9uZzEPMA0GA1UEBwwGWmh1SGFpMQ8wDQYDVQQKDAZpcmVzdHkxETAPBgNVBAMM\nCHRlc3QuY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyCM0rqJe\ncvgnCfOw4fATotPwk5Ba0gC2YvIrO+gSbQkyxXF5jhZB3W6BkWUWR4oNFLLSqcVb\nVDPitz/Mt46Mo8amuS6zTbQetGnBARzPLtmVhJfoeLj0efMiOepOSZflj9Ob4yKR\n2bGdEFOdHPjm+4ggXU9jMKeLqdVvxll/JiVFBW5smPtW1Oc/BV5terhscJdOgmRr\nabf9xiIis9/qVYfyGn52u9452V0owUuwP7nZ01jt6iMWEGeQU6mwPENgvj1olji2\nWjdG2UwpUVp3jp3l7j1ekQ6mI0F7yI+LeHzfUwiyVt1TmtMWn1ztk6FfLRqwJWR/\nEvm95vnfS3Le4S2ky3XAgn2UnCMyej3wDN6qHR1onpRVeXhrBajbCRDRBMwaNw/1\n/3Uvza8QKK10PzQR6OcQ0xo9psMkd9j9ts/dTuo2fzaqpIfyUbPST4GdqNG9NyIh\n/B9g26/0EWcjyO7mYVkaycrtLMaXm1u9jyRmcQQI1cGrGwyXbrieNp63AgMBAAGj\ncTBvMB0GA1UdDgQWBBSZtSvV8mBwl0bpkvFtgyiOUUcbszAfBgNVHSMEGDAWgBSZ\ntSvV8mBwl0bpkvFtgyiOUUcbszAMBgNVHRMEBTADAQH/MB8GA1UdEQQYMBaCCHRl\nc3QuY29tggoqLnRlc3QuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAHGEul/x7ViVgC\ntC8CbXEslYEkj1XVr2Y4hXZXAXKd3W7V3TC8rqWWBbr6L/tsSVFt126V5WyRmOaY\n1A5pju8VhnkhYxYfZALQxJN2tZPFVeME9iGJ9BE1wPtpMgITX8Rt9kbNlENfAgOl\nPYzrUZN1YUQjX+X8t8/1VkSmyZysr6ngJ46/M8F16gfYXc9zFj846Z9VST0zCKob\nrJs3GtHOkS9zGGldqKKCj+Awl0jvTstI4qtS1ED92tcnJh5j/SSXCAB5FgnpKZWy\nhme45nBQj86rJ8FhN+/aQ9H9/2Ib6Q4wbpaIvf4lQdLUEcWAeZGW6Rk0JURwEog1\n7/mMgkapDglgeFx9f/XztSTrkHTaX4Obr+nYrZ2V4KOB4llZnK5GeNjDrOOJDk2y\nIJFgBOZJWyS93dQfuKEj42hA79MuX64lMSCVQSjX+ipR289GQZqFrIhiJxLyA+Ve\nU/OOcSRr39Kuis/JJ+DkgHYa/PWHZhnJQBxcqXXk1bJGw9BNbhM=\n-----END CERTIFICATE-----\n", - "key": "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n", - "sni": "test.com" -}' - -#test grpc proxy -curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' -{ - "methods": ["POST"], - "uri": "/helloworld.Greeter/SayHello", - "upstream": { - "scheme": "grpc", - "type": "roundrobin", - "nodes": { - "127.0.0.1:50051": 1 - } - } -}' - -# test grpc proxy with plaintext -grpcurl -plaintext -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' 127.0.0.1:9081 helloworld.Greeter.SayHello | grep 'Hello apisix' - -# test grpc proxy with ssl -grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' - -# the old way -curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' -{ - "methods": ["POST"], - "uri": "/helloworld.Greeter/SayHello", - "service_protocol": "grpc", - "upstream": { - "type": "roundrobin", - "nodes": { - "127.0.0.1:50051": 1 - } - } -}' - -grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' - -#test grpcs proxy -curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' -{ - "methods": ["POST"], - "uri": "/helloworld.Greeter/SayHello", - "upstream": { - "scheme": "grpcs", - "type": "roundrobin", - "nodes": { - "127.0.0.1:50052": 1 - } - } -}' - -grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' - -if ! openresty -V 2>&1 | grep "apisix-nginx-module"; then - echo "skip vanilla OpenResty" - clean_up - exit 0 -fi - -#test grpcs with mTLS proxy -curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' -{ - "methods": ["POST"], - "uri": "/helloworld.Greeter/SayHello", - "upstream": { - "scheme": "grpcs", - "tls": { - "client_cert": "-----BEGIN CERTIFICATE-----\nMIIDOjCCAiICAwD6zzANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJjbjESMBAG\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDTALBgNVBAoMBGFwaTcx\nDDAKBgNVBAsMA29wczEWMBQGA1UEAwwNY2EuYXBpc2l4LmRldjAeFw0yMDA2MjAx\nMzE1MDBaFw0zMDA3MDgxMzE1MDBaMF0xCzAJBgNVBAYTAmNuMRIwEAYDVQQIDAlH\ndWFuZ0RvbmcxDTALBgNVBAoMBGFwaTcxDzANBgNVBAcMBlpodUhhaTEaMBgGA1UE\nAwwRY2xpZW50LmFwaXNpeC5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQCfKI8uiEH/ifZikSnRa3/E2B4ohVWRwjo/IxyDEWomgR4tLk1pSJhP/4SC\nLWuMQTFWTbSqt1IFYy4ZbVSHHyGoNPmJGrHRJCGE+sgpfzn0GjV4lXQPJD0k6GR1\nCX2Mo1TWdFqSJ/Hc5AQwcQFnPfoLAwsBy4yqrlmf96ZAUytl/7Zkjf4P7mJkJHtM\n/WgSR0pGhjZTAGRf5DJWoO51ki3i3JI+15mOhmnnCpnksnGVPfl92q92Hz/4v3iq\nE+UThPYRpcGbnddzMvPaCXiavg8B/u2LVbn4l0adamqQGepOAjD/1xraOVP2W22W\n0PztDXJ4rLe+capNS4oGuSUfkIENAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHKn\nHxUhuk/nL2Sg5UB84OoJe5XPgNBvVMKN0c/NAPKVIPninvUcG/mHeKexPzE0sMga\nRNos75N2199EXydqUcsJ8jL0cNtQ2k5JQXXg0ntNC4tuCgIKAOnO879y5hSG36e5\n7wmAoVKnabgjej09zG1kkXvAmpgqoxeVCu7h7fK+AurLbsGCTaHoA5pG1tcHDxJQ\nfpVcbBfwQDSBW3SQjiRqX453/01nw6kbOeLKYraJysaG8ZU2K8+WpW6JDubciHjw\nfQnpU2U16XKivhxeuKYrV/INL0sxj/fZraNYErvJWzh5llvIdNLmeSPmvb50JUIs\n+lDqn1MobTXzDpuCFXA=\n-----END CERTIFICATE-----\n", - "client_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAnyiPLohB/4n2YpEp0Wt/xNgeKIVVkcI6PyMcgxFqJoEeLS5N\naUiYT/+Egi1rjEExVk20qrdSBWMuGW1Uhx8hqDT5iRqx0SQhhPrIKX859Bo1eJV0\nDyQ9JOhkdQl9jKNU1nRakifx3OQEMHEBZz36CwMLAcuMqq5Zn/emQFMrZf+2ZI3+\nD+5iZCR7TP1oEkdKRoY2UwBkX+QyVqDudZIt4tySPteZjoZp5wqZ5LJxlT35fdqv\ndh8/+L94qhPlE4T2EaXBm53XczLz2gl4mr4PAf7ti1W5+JdGnWpqkBnqTgIw/9ca\n2jlT9lttltD87Q1yeKy3vnGqTUuKBrklH5CBDQIDAQABAoIBAHDe5bPdQ9jCcW3z\nfpGax/DER5b6//UvpfkSoGy/E+Wcmdb2yEVLC2FoVwOuzF+Z+DA5SU/sVAmoDZBQ\nvapZxJeygejeeo5ULkVNSFhNdr8LOzJ54uW+EHK1MFDj2xq61jaEK5sNIvRA7Eui\nSJl8FXBrxwmN3gNJRBwzF770fImHUfZt0YU3rWKw5Qin7QnlUzW2KPUltnSEq/xB\nkIzyWpuj7iAm9wTjH9Vy06sWCmxj1lzTTXlanjPb1jOTaOhbQMpyaAzRgQN8PZiE\nYKCarzVj7BJr7/vZYpnQtQDY12UL5n33BEqMP0VNHVqv+ZO3bktfvlwBru5ZJ7Cf\nURLsSc0CgYEAyz7FzV7cZYgjfUFD67MIS1HtVk7SX0UiYCsrGy8zA19tkhe3XVpc\nCZSwkjzjdEk0zEwiNAtawrDlR1m2kverbhhCHqXUOHwEpujMBjeJCNUVEh3OABr8\nvf2WJ6D1IRh8FA5CYLZP7aZ41fcxAnvIPAEThemLQL3C4H5H5NG2WFsCgYEAyHhP\nonpS/Eo/OXKYFLR/mvjizRVSomz1lVVL+GWMUYQsmgsPyBJgyAOX3Pqt9catgxhM\nDbEr7EWTxth3YeVzamiJPNVK0HvCax9gQ0KkOmtbrfN54zBHOJ+ieYhsieZLMgjx\niu7Ieo6LDGV39HkvekzutZpypiCpKlMaFlCFiLcCgYEAmAgRsEj4Nh665VPvuZzH\nZIgZMAlwBgHR7/v6l7AbybcVYEXLTNJtrGEEH6/aOL8V9ogwwZuIvb/TEidCkfcf\nzg/pTcGf2My0MiJLk47xO6EgzNdso9mMG5ZYPraBBsuo7NupvWxCp7NyCiOJDqGH\nK5NmhjInjzsjTghIQRq5+qcCgYEAxnm/NjjvslL8F69p/I3cDJ2/RpaG0sMXvbrO\nVWaMryQyWGz9OfNgGIbeMu2Jj90dar6ChcfUmb8lGOi2AZl/VGmc/jqaMKFnElHl\nJ5JyMFicUzPMiG8DBH+gB71W4Iy+BBKwugHBQP2hkytewQ++PtKuP+RjADEz6vCN\n0mv0WS8CgYBnbMRP8wIOLJPRMw/iL9BdMf606X4xbmNn9HWVp2mH9D3D51kDFvls\n7y2vEaYkFv3XoYgVN9ZHDUbM/YTUozKjcAcvz0syLQb8wRwKeo+XSmo09+360r18\nzRugoE7bPl39WdGWaW3td0qf1r9z3sE2iWUTJPRQ3DYpsLOYIgyKmw==\n-----END RSA PRIVATE KEY-----\n" - }, - "type": "roundrobin", - "nodes": { - "127.0.0.1:50053": 1 - } - } -}' - -grpcurl -insecure -import-path ./build-cache/proto -proto helloworld.proto -d '{"name":"apisix"}' test.com:9443 helloworld.Greeter.SayHello | grep 'Hello apisix' -clean_up diff --git a/t/grpc_server_example b/t/grpc_server_example new file mode 160000 index 000000000000..2df0e72ebacf --- /dev/null +++ b/t/grpc_server_example @@ -0,0 +1 @@ +Subproject commit 2df0e72ebacfb306fcfb3d61f27583017d0c3c10 diff --git a/t/node/grpc-proxy-mtls.t b/t/node/grpc-proxy-mtls.t index 472acf1782d8..864099341b7a 100644 --- a/t/node/grpc-proxy-mtls.t +++ b/t/node/grpc-proxy-mtls.t @@ -66,7 +66,7 @@ routes: location /t { content_by_lua_block { local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") if not proc then ngx.say(err) return @@ -84,3 +84,51 @@ routes: { "message": "Hello apisix" } + + + +=== TEST 2: Bidirectional API grpcs proxy test with mTLS +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHelloBidirectionalStream + methods: [ + POST + ] + upstream: + scheme: grpcs + tls: + client_cert: "-----BEGIN CERTIFICATE-----\nMIIDOjCCAiICAwD6zzANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJjbjESMBAG\nA1UECAwJR3VhbmdEb25nMQ8wDQYDVQQHDAZaaHVIYWkxDTALBgNVBAoMBGFwaTcx\nDDAKBgNVBAsMA29wczEWMBQGA1UEAwwNY2EuYXBpc2l4LmRldjAeFw0yMDA2MjAx\nMzE1MDBaFw0zMDA3MDgxMzE1MDBaMF0xCzAJBgNVBAYTAmNuMRIwEAYDVQQIDAlH\ndWFuZ0RvbmcxDTALBgNVBAoMBGFwaTcxDzANBgNVBAcMBlpodUhhaTEaMBgGA1UE\nAwwRY2xpZW50LmFwaXNpeC5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQCfKI8uiEH/ifZikSnRa3/E2B4ohVWRwjo/IxyDEWomgR4tLk1pSJhP/4SC\nLWuMQTFWTbSqt1IFYy4ZbVSHHyGoNPmJGrHRJCGE+sgpfzn0GjV4lXQPJD0k6GR1\nCX2Mo1TWdFqSJ/Hc5AQwcQFnPfoLAwsBy4yqrlmf96ZAUytl/7Zkjf4P7mJkJHtM\n/WgSR0pGhjZTAGRf5DJWoO51ki3i3JI+15mOhmnnCpnksnGVPfl92q92Hz/4v3iq\nE+UThPYRpcGbnddzMvPaCXiavg8B/u2LVbn4l0adamqQGepOAjD/1xraOVP2W22W\n0PztDXJ4rLe+capNS4oGuSUfkIENAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHKn\nHxUhuk/nL2Sg5UB84OoJe5XPgNBvVMKN0c/NAPKVIPninvUcG/mHeKexPzE0sMga\nRNos75N2199EXydqUcsJ8jL0cNtQ2k5JQXXg0ntNC4tuCgIKAOnO879y5hSG36e5\n7wmAoVKnabgjej09zG1kkXvAmpgqoxeVCu7h7fK+AurLbsGCTaHoA5pG1tcHDxJQ\nfpVcbBfwQDSBW3SQjiRqX453/01nw6kbOeLKYraJysaG8ZU2K8+WpW6JDubciHjw\nfQnpU2U16XKivhxeuKYrV/INL0sxj/fZraNYErvJWzh5llvIdNLmeSPmvb50JUIs\n+lDqn1MobTXzDpuCFXA=\n-----END CERTIFICATE-----\n", + client_key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAnyiPLohB/4n2YpEp0Wt/xNgeKIVVkcI6PyMcgxFqJoEeLS5N\naUiYT/+Egi1rjEExVk20qrdSBWMuGW1Uhx8hqDT5iRqx0SQhhPrIKX859Bo1eJV0\nDyQ9JOhkdQl9jKNU1nRakifx3OQEMHEBZz36CwMLAcuMqq5Zn/emQFMrZf+2ZI3+\nD+5iZCR7TP1oEkdKRoY2UwBkX+QyVqDudZIt4tySPteZjoZp5wqZ5LJxlT35fdqv\ndh8/+L94qhPlE4T2EaXBm53XczLz2gl4mr4PAf7ti1W5+JdGnWpqkBnqTgIw/9ca\n2jlT9lttltD87Q1yeKy3vnGqTUuKBrklH5CBDQIDAQABAoIBAHDe5bPdQ9jCcW3z\nfpGax/DER5b6//UvpfkSoGy/E+Wcmdb2yEVLC2FoVwOuzF+Z+DA5SU/sVAmoDZBQ\nvapZxJeygejeeo5ULkVNSFhNdr8LOzJ54uW+EHK1MFDj2xq61jaEK5sNIvRA7Eui\nSJl8FXBrxwmN3gNJRBwzF770fImHUfZt0YU3rWKw5Qin7QnlUzW2KPUltnSEq/xB\nkIzyWpuj7iAm9wTjH9Vy06sWCmxj1lzTTXlanjPb1jOTaOhbQMpyaAzRgQN8PZiE\nYKCarzVj7BJr7/vZYpnQtQDY12UL5n33BEqMP0VNHVqv+ZO3bktfvlwBru5ZJ7Cf\nURLsSc0CgYEAyz7FzV7cZYgjfUFD67MIS1HtVk7SX0UiYCsrGy8zA19tkhe3XVpc\nCZSwkjzjdEk0zEwiNAtawrDlR1m2kverbhhCHqXUOHwEpujMBjeJCNUVEh3OABr8\nvf2WJ6D1IRh8FA5CYLZP7aZ41fcxAnvIPAEThemLQL3C4H5H5NG2WFsCgYEAyHhP\nonpS/Eo/OXKYFLR/mvjizRVSomz1lVVL+GWMUYQsmgsPyBJgyAOX3Pqt9catgxhM\nDbEr7EWTxth3YeVzamiJPNVK0HvCax9gQ0KkOmtbrfN54zBHOJ+ieYhsieZLMgjx\niu7Ieo6LDGV39HkvekzutZpypiCpKlMaFlCFiLcCgYEAmAgRsEj4Nh665VPvuZzH\nZIgZMAlwBgHR7/v6l7AbybcVYEXLTNJtrGEEH6/aOL8V9ogwwZuIvb/TEidCkfcf\nzg/pTcGf2My0MiJLk47xO6EgzNdso9mMG5ZYPraBBsuo7NupvWxCp7NyCiOJDqGH\nK5NmhjInjzsjTghIQRq5+qcCgYEAxnm/NjjvslL8F69p/I3cDJ2/RpaG0sMXvbrO\nVWaMryQyWGz9OfNgGIbeMu2Jj90dar6ChcfUmb8lGOi2AZl/VGmc/jqaMKFnElHl\nJ5JyMFicUzPMiG8DBH+gB71W4Iy+BBKwugHBQP2hkytewQ++PtKuP+RjADEz6vCN\n0mv0WS8CgYBnbMRP8wIOLJPRMw/iL9BdMf606X4xbmNn9HWVp2mH9D3D51kDFvls\n7y2vEaYkFv3XoYgVN9ZHDUbM/YTUozKjcAcvz0syLQb8wRwKeo+XSmo09+360r18\nzRugoE7bPl39WdGWaW3td0qf1r9z3sE2iWUTJPRQ3DYpsLOYIgyKmw==\n-----END RSA PRIVATE KEY-----\n" + nodes: + "127.0.0.1:50053": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} +{ + "message": "stream ended" +} diff --git a/t/node/grpc-proxy-stream.t b/t/node/grpc-proxy-stream.t new file mode 100644 index 000000000000..21100d88bc47 --- /dev/null +++ b/t/node/grpc-proxy-stream.t @@ -0,0 +1,185 @@ +# +# 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'; + +no_long_string(); +no_root_location(); +no_shuffle(); +add_block_preprocessor(sub { + my ($block) = @_; + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: Test server side streaming method through gRPC proxy +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHelloServerStream + methods: [ + POST + ] + upstream: + scheme: grpc + nodes: + "127.0.0.1:50051": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local opts = { + merge_stderr = true + } + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloServerStream", opts) + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} + + + +=== TEST 2: Test client side streaming method through gRPC proxy +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHelloClientStream + methods: [ + POST + ] + upstream: + scheme: grpc + nodes: + "127.0.0.1:50051": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloClientStream") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix!Hello apisix!Hello apisix!Hello apisix!" +} + + + +=== TEST 3: Test bidirectional streaming method through gRPC proxy +--- http2 +--- apisix_yaml +routes: + - + id: 1 + uris: + - /helloworld.Greeter/SayHelloBidirectionalStream + methods: [ + POST + ] + upstream: + scheme: grpc + nodes: + "127.0.0.1:50051": 1 + type: roundrobin +#END +--- config + location /t { + content_by_lua_block { + local ngx_pipe = require("ngx.pipe") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream") + if not proc then + ngx.say(err) + return + end + local data, err = proc:stdout_read_all() + if not data then + ngx.say(err) + return + end + ngx.say(data:sub(1, -2)) + return + } + } +--- response_body +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} +{ + "message": "Hello apisix" +} +{ + "message": "stream ended" +} diff --git a/t/node/grpc-proxy-unary.t b/t/node/grpc-proxy-unary.t index 5397179dc6df..01a980104333 100644 --- a/t/node/grpc-proxy-unary.t +++ b/t/node/grpc-proxy-unary.t @@ -57,7 +57,7 @@ routes: local opts= { merge_stderr = true, } - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello", opts) + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello", opts) if not proc then ngx.say(err) return @@ -99,7 +99,7 @@ routes: location /t { content_by_lua_block { local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") if not proc then ngx.say(err) return @@ -141,7 +141,7 @@ routes: location /t { content_by_lua_block { local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") if not proc then ngx.say(err) return @@ -189,7 +189,7 @@ routes: location /t { content_by_lua_block { local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./build-cache/proto -proto helloworld.proto -insecure -d '{\"name\":\"apisix\"}' test.com:1994 helloworld.Greeter.SayHello") + local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -insecure -d '{\"name\":\"apisix\"}' test.com:1994 helloworld.Greeter.SayHello") if not proc then ngx.say(err) return From bba6a0012b317bff656bdb614ae5e33150fc1920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Thu, 28 Oct 2021 08:57:49 +0800 Subject: [PATCH 042/260] perf(proxy-cache): flush the body chunk during caching (#5340) --- apisix/core/response.lua | 7 +++++-- apisix/plugins/proxy-cache/memory_handler.lua | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apisix/core/response.lua b/apisix/core/response.lua index 7b1128d249a0..e1f0f4d753a6 100644 --- a/apisix/core/response.lua +++ b/apisix/core/response.lua @@ -164,7 +164,7 @@ end -- ... -- -- Inspired by kong.response.get_raw_body() -function _M.hold_body_chunk(ctx) +function _M.hold_body_chunk(ctx, hold_the_copy) local body_buffer local chunk, eof = arg[1], arg[2] if eof then @@ -193,7 +193,10 @@ function _M.hold_body_chunk(ctx) end end - arg[1] = nil + if not hold_the_copy then + -- flush the origin body chunk + arg[1] = nil + end return nil end diff --git a/apisix/plugins/proxy-cache/memory_handler.lua b/apisix/plugins/proxy-cache/memory_handler.lua index 2fd4d111ea69..5a76f9e52919 100644 --- a/apisix/plugins/proxy-cache/memory_handler.lua +++ b/apisix/plugins/proxy-cache/memory_handler.lua @@ -299,7 +299,7 @@ function _M.body_filter(conf, ctx) return end - local res_body = core.response.hold_body_chunk(ctx) + local res_body = core.response.hold_body_chunk(ctx, true) if not res_body then return end @@ -318,8 +318,6 @@ function _M.body_filter(conf, ctx) if not res then core.log.error("failed to set cache, err: ", err) end - - ngx.arg[1] = res_body end From 2bb86f6a97c919e310e02c7824f7f559aa7e295a Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Fri, 29 Oct 2021 17:25:43 +0800 Subject: [PATCH 043/260] fix(request-validation): correct rejected_message to rejected_msg (#5355) --- docs/en/latest/plugins/request-validation.md | 6 +++--- docs/zh/latest/plugins/request-validation.md | 6 +++--- t/plugin/request-validation.t | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/en/latest/plugins/request-validation.md b/docs/en/latest/plugins/request-validation.md index a474ae69f091..d2a17b2ba8c4 100644 --- a/docs/en/latest/plugins/request-validation.md +++ b/docs/en/latest/plugins/request-validation.md @@ -45,7 +45,7 @@ For more information on schema, refer to [JSON schema](https://github.com/api7/j | ---------------- | ------ | ----------- | ------- | ----- | -------------------------- | | header_schema | object | optional | | | schema for the header data | | body_schema | object | optional | | | schema for the body data | -| rejected_message | string | optional | | | the custom rejected message | +| rejected_msg | string | optional | | | the custom rejected message | ## How To Enable @@ -64,7 +64,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f13 "required_payload": {"type": "string"}, "boolean_payload": {"type": "boolean"} }, - "rejected_message": "customize reject message" + "rejected_msg": "customize reject message" } } }, @@ -271,7 +271,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f13 "required_payload": {"type": "string"}, "boolean_payload": {"type": "boolean"} }, - "rejected_message": "customize reject message" + "rejected_msg": "customize reject message" } } }, diff --git a/docs/zh/latest/plugins/request-validation.md b/docs/zh/latest/plugins/request-validation.md index c332bf43f0ae..a1ba46d5ddb1 100644 --- a/docs/zh/latest/plugins/request-validation.md +++ b/docs/zh/latest/plugins/request-validation.md @@ -44,7 +44,7 @@ title: request-validation | ---------------- | ------ | ----------- | ------- | ----- | --------------------------------- | | header_schema | object | 可选 | | | `header` 数据的 `schema` 数据结构 | | body_schema | object | 可选 | | | `body` 数据的 `schema` 数据结构 | -| rejected_message | string | 可选 | | | 自定义拒绝信息 | +| rejected_msg | string | 可选 | | | 自定义拒绝信息 | ## 如何启用 @@ -63,7 +63,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f13 "required_payload": {"type": "string"}, "boolean_payload": {"type": "boolean"} }, - "rejected_message": "customize reject message" + "rejected_msg": "customize reject message" } } }, @@ -269,7 +269,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f13 "required_payload": {"type": "string"}, "boolean_payload": {"type": "boolean"} }, - "rejected_message": "customize reject message" + "rejected_msg": "customize reject message" } } }, diff --git a/t/plugin/request-validation.t b/t/plugin/request-validation.t index 35edd031540f..711bf22585a4 100644 --- a/t/plugin/request-validation.t +++ b/t/plugin/request-validation.t @@ -1633,7 +1633,7 @@ qr/string too long/ [[{ "plugins": { "request-validation": { - "rejected_message": "customize reject message" + "rejected_msg": "customize reject message" } }, "upstream": { From be43ba9dd291dc6ebcc18d160c159c8b7b0b2014 Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Sun, 31 Oct 2021 19:19:30 +0800 Subject: [PATCH 044/260] docs: bind route to correct upstream_id in the example (#5380) --- docs/zh/latest/architecture-design/upstream.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/latest/architecture-design/upstream.md b/docs/zh/latest/architecture-design/upstream.md index 56eb7ffa706a..532713461633 100644 --- a/docs/zh/latest/architecture-design/upstream.md +++ b/docs/zh/latest/architecture-design/upstream.md @@ -54,7 +54,7 @@ curl http://127.0.0.1:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335 curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/index.html", - "upstream_id": 2 + "upstream_id": 1 }' ``` From a62fd939321287b904d95aa25679c0dbb52c211c Mon Sep 17 00:00:00 2001 From: Yujia Qiao <rapiz3142@gmail.com> Date: Sun, 31 Oct 2021 19:22:42 +0800 Subject: [PATCH 045/260] docs: improve docs about testing (#5377) --- CONTRIBUTING.md | 1 + docs/en/latest/how-to-build.md | 4 +++- docs/zh/latest/how-to-build.md | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf69bb93f548..a41891431d94 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,6 +126,7 @@ Once we've discussed your changes and you've got your code ready, make sure that * Use tool to check your test case style statically by command, eg: `make lint`. * When the test file is too large, for example > 800 lines, you should split it to a new file. Please take a look at `t/plugin/limit-conn.t` and `t/plugin/limit-conn2.t`. + * For more details, see the [testing framework](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md) ## Do you have questions about the source code? diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 8a2d898bffb9..c72adcaaf8a6 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -178,7 +178,7 @@ apisix help git clone https://github.com/iresty/test-nginx.git ``` -4. Load the test-nginx library with the `prove` command in `perl` and run the test case set in the `/t` directory. +4. Here are two ways of running tests: - Append the current directory to the perl module directory: `export PERL5LIB=.:$PERL5LIB`, then run `make test` command. @@ -220,6 +220,8 @@ Run the specified test case using the following command. prove -Itest-nginx/lib -r t/plugin/openid-connect.t ``` +For more details on the test cases, see the [testing framwork](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md). + ## Step 5: Update Admin API token to Protect Apache APISIX You need to modify the Admin API key to protect Apache APISIX. diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index bfbcd83675e9..39e98f0921cb 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -176,7 +176,7 @@ apisix help git clone https://github.com/iresty/test-nginx.git ``` -4. 通过 `perl` 的 `prove` 命令来加载 test-nginx 的库,并运行 `/t` 目录下的测试案例集: +4. 有两种方法运行测试: - 追加当前目录到perl模块目录: `export PERL5LIB=.:$PERL5LIB`,然后运行 `make test` 命令。 @@ -218,6 +218,8 @@ apisix help prove -Itest-nginx/lib -r t/plugin/openid-connect.t ``` +关于测试用例的更多细节,参见[测试框架](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md) + ## 步骤5:修改 Admin API key 您需要修改 Admin API 的 key,以保护 Apache APISIX。 From c700c43bfbafa3e74848d7e1eae5df8dc3338013 Mon Sep 17 00:00:00 2001 From: Joey <majunjie@apache.org> Date: Mon, 1 Nov 2021 08:48:22 +0800 Subject: [PATCH 046/260] chore: Bump apisix-build-tools to v2.5.0 (#5375) --- .github/workflows/centos7-ci.yml | 2 +- utils/linux-install-openresty.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 899a33ceb896..c63d27462888 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -44,7 +44,7 @@ jobs: run: | export VERSION=${{ steps.branch_env.outputs.version }} sudo gem install --no-document fpm - git clone -b v2.2.1 https://github.com/api7/apisix-build-tools.git + git clone -b v2.5.0 https://github.com/api7/apisix-build-tools.git # move codes under build tool mkdir ./apisix-build-tools/apisix diff --git a/utils/linux-install-openresty.sh b/utils/linux-install-openresty.sh index 8dbb7169d47b..ce0d8a48bcb8 100755 --- a/utils/linux-install-openresty.sh +++ b/utils/linux-install-openresty.sh @@ -26,7 +26,7 @@ sudo apt-get update if [ "$OPENRESTY_VERSION" == "source" ]; then cd .. - wget https://raw.githubusercontent.com/api7/apisix-build-tools/v2.4.0/build-apisix-base.sh + wget https://raw.githubusercontent.com/api7/apisix-build-tools/v2.5.0/build-apisix-base.sh chmod +x build-apisix-base.sh ./build-apisix-base.sh latest From 2400cd7bb49b13470c9b2c2f2a57aa93e19817bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Mon, 1 Nov 2021 09:20:42 +0800 Subject: [PATCH 047/260] docs: fix the code block language (#5381) --- docs/en/latest/architecture-design/upstream.md | 2 +- docs/zh/latest/architecture-design/upstream.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/architecture-design/upstream.md b/docs/en/latest/architecture-design/upstream.md index 8f07883ebc00..384ac8718b57 100644 --- a/docs/en/latest/architecture-design/upstream.md +++ b/docs/en/latest/architecture-design/upstream.md @@ -35,7 +35,7 @@ In addition to the basic complex equalization algorithm selection, APISIX's Upst Create an upstream object use case: -```json +```shell curl http://127.0.0.1:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "type": "chash", diff --git a/docs/zh/latest/architecture-design/upstream.md b/docs/zh/latest/architecture-design/upstream.md index 532713461633..3941410a93e3 100644 --- a/docs/zh/latest/architecture-design/upstream.md +++ b/docs/zh/latest/architecture-design/upstream.md @@ -36,7 +36,7 @@ APISIX 的 Upstream 除了基本的负载均衡算法选择外,还支持对上 创建上游对象用例: -```json +```shell curl http://127.0.0.1:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "type": "chash", From e79bddb19f770523d66e51a152923e1f2344c04d Mon Sep 17 00:00:00 2001 From: Yujia Qiao <rapiz3142@gmail.com> Date: Mon, 1 Nov 2021 09:35:05 +0800 Subject: [PATCH 048/260] feat: Support installation on arch (#5350) --- docs/en/latest/install-dependencies.md | 4 +++- docs/zh/latest/install-dependencies.md | 4 +++- utils/install-dependencies.sh | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/install-dependencies.md b/docs/en/latest/install-dependencies.md index eb8068791580..048e580b3788 100644 --- a/docs/en/latest/install-dependencies.md +++ b/docs/en/latest/install-dependencies.md @@ -44,7 +44,9 @@ title: Install Dependencies Run the following command to install Apache APISIX's dependencies on a supported operating system. -Supported OS versions: CentOS7, Fedora31 & 32, Ubuntu 16.04 & 18.04, Debian 9 & 10, Mac OSX +Supported OS versions: CentOS7, Fedora31 & 32, Ubuntu 16.04 & 18.04, Debian 9 & 10, Arch Linux, Mac OSX + +Note that in the case of Arch Linux, we use `openresty` from the AUR, thus requiring a AUR helper. For now `yay` and `pacaur` are supported. ``` curl https://raw.githubusercontent.com/apache/apisix/master/utils/install-dependencies.sh -sL | bash - diff --git a/docs/zh/latest/install-dependencies.md b/docs/zh/latest/install-dependencies.md index 07d27cef825d..1d97dc97092d 100644 --- a/docs/zh/latest/install-dependencies.md +++ b/docs/zh/latest/install-dependencies.md @@ -44,7 +44,9 @@ title: 安装依赖 在支持的操作系统上运行以下指令即可安装 Apache APISIX dependencies。 -支持的操作系统版本: CentOS 7, Fedora 31 & 32, Ubuntu 16.04 & 18.04, Debian 9 & 10, Mac OSX。 +支持的操作系统版本: CentOS 7, Fedora 31 & 32, Ubuntu 16.04 & 18.04, Debian 9 & 10, Arch Linux, Mac OSX。 + +注意,对于 Arch Linux 来说,我们使用 AUR 源中的 `openresty`,所以需要 AUR Helper 才能正常安装。目前支持 `yay` 和 `pacaur`。 ``` curl https://raw.githubusercontent.com/apache/apisix/master/utils/install-dependencies.sh -sL | bash - diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh index 19a7e338d748..e7cc5da0738c 100755 --- a/utils/install-dependencies.sh +++ b/utils/install-dependencies.sh @@ -19,6 +19,29 @@ set -ex +function detect_aur_helper() { + if [[ $(which yay) ]]; then + AUR_HELPER=yay + elif [[ $(which pacaur) ]]; then + AUR_HELPER=pacaur + else + echo No available AUR helpers found. Please specify your AUR helper by AUR_HELPER. + exit -1 + fi +} + +function install_dependencies_with_aur() { + detect_aur_helper + $AUR_HELPER -S openresty --noconfirm + sudo pacman -S openssl --noconfirm + + export OPENRESTY_PREFIX=/opt/openresty + + sudo mkdir $OPENRESTY_PREFIX/openssl + sudo ln -s /usr/include $OPENRESTY_PREFIX/openssl/include + sudo ln -s /usr/lib $OPENRESTY_PREFIX/openssl/lib +} + # Install dependencies on centos and fedora function install_dependencies_with_yum() { # add OpenResty source @@ -66,6 +89,8 @@ function multi_distro_installation() { install_dependencies_with_apt "debian" elif grep -Eqi "Ubuntu" /etc/issue || grep -Eq "Ubuntu" /etc/*-release; then install_dependencies_with_apt "ubuntu" + elif grep -Eqi "Arch" /etc/issue || grep -Eq "Arch" /etc/*-release; then + install_dependencies_with_aur else echo "Non-supported operating system version" fi From 7df2ea996c336e630acee08b9b546644dea5e7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Mon, 1 Nov 2021 10:28:18 +0800 Subject: [PATCH 049/260] docs: document the PR manners of APISIX (#5383) --- .github/PULL_REQUEST_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 615ab3162142..e43448eb7c64 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,15 @@ ### Pre-submission checklist: +<!-- +Please follow the requirements: +1. Use Draft if the PR is not ready to be reviewed +2. Test is required for the feat/fix PR, unless you have a good reason +3. Doc is required for the feat PR +4. Use a new commit to resolve review instead of `push -f` +5. Use "request review" to notify the reviewer once you have resolved the review +--> + * [ ] Did you explain what problem does this PR solve? Or what new features have been added? * [ ] Have you added corresponding test cases? * [ ] Have you modified the corresponding document? From 759e6663162d43e761d556b85e3061f755ea74ae Mon Sep 17 00:00:00 2001 From: Joey <majunjie@apache.org> Date: Mon, 1 Nov 2021 10:28:43 +0800 Subject: [PATCH 050/260] docs: Update RPM installation guide for apisix 2.10.1 (#5384) --- docs/en/latest/how-to-build.md | 10 +++++----- docs/zh/latest/how-to-build.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index c72adcaaf8a6..98b00bcd3ff3 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -37,11 +37,13 @@ This installation method is suitable for CentOS 7. For now, the Apache APISIX RP ```shell sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo -# View apisix package information, only 2.10.0 is included for now +# View the information of the latest apisix package sudo yum info -y apisix + +# Will show the existing apisix packages sudo yum --showduplicates list apisix -# Will install apisix-2.10.0 +# Will install the latest apisix package sudo yum install apisix ``` @@ -56,11 +58,9 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep This installation method is suitable for CentOS 7, please run the following command to install Apache APISIX. ```shell -sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.1/apisix-2.10.1-0.el7.x86_64.rpm +sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.1-0.el7.x86_64.rpm ``` -> You can also install the RPM package via running `sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.0-0.el7.x86_64.rpm`. - ### Installation via Docker Please refer to: [Installing Apache APISIX with Docker](https://hub.docker.com/r/apache/apisix). diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index 39e98f0921cb..83ea1821d8b1 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -37,11 +37,13 @@ Apache APISIX 的运行环境需要依赖 NGINX 和 etcd,所以在安装 Apach ```shell sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo -# View apisix package information, only 2.10.0 is included for now +# View the information of the latest apisix package sudo yum info -y apisix + +# Will show the existing apisix packages sudo yum --showduplicates list apisix -# Will install apisix-2.10.0 +# Will install the latest apisix package sudo yum install apisix ``` @@ -56,11 +58,9 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep 这种安装方式适用于 CentOS 7 操作系统,请运行以下命令安装 Apache APISIX。 ```shell -sudo yum install -y https://github.com/apache/apisix/releases/download/2.10.1/apisix-2.10.1-0.el7.x86_64.rpm +sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.1-0.el7.x86_64.rpm ``` -> 您也可以运行 `sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.0-0.el7.x86_64.rpm` 命令安装。 - ### 通过 Docker 安装 详情请参考:[使用 Docker 安装 Apache APISIX](https://hub.docker.com/r/apache/apisix)。 From 9fc67b1c394a48f1fe9dc3cd500d537399f5153a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Mon, 1 Nov 2021 15:36:03 +0800 Subject: [PATCH 051/260] test: the http2 is missing which causes weird result (#5382) --- t/APISIX.pm | 2 +- t/node/grpc-proxy-unary.t | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/t/APISIX.pm b/t/APISIX.pm index da9a46e75e84..1f0f18a7f947 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -599,7 +599,7 @@ _EOC_ $config .= <<_EOC_; $ipv6_listen_conf - listen 1994 ssl; + listen 1994 ssl http2; ssl_certificate cert/apisix.crt; ssl_certificate_key cert/apisix.key; lua_ssl_trusted_certificate cert/apisix.crt; diff --git a/t/node/grpc-proxy-unary.t b/t/node/grpc-proxy-unary.t index 01a980104333..330b9b44a2da 100644 --- a/t/node/grpc-proxy-unary.t +++ b/t/node/grpc-proxy-unary.t @@ -68,7 +68,6 @@ routes: return end ngx.say(data:sub(1, -2)) - return } } --- response_body @@ -110,7 +109,6 @@ routes: return end ngx.say(data:sub(1, -2)) - return } } --- response_body @@ -152,7 +150,6 @@ routes: return end ngx.say(data:sub(1, -2)) - return } } --- response_body @@ -200,7 +197,6 @@ routes: return end ngx.say(data:sub(1, -2)) - return } } --- response_body From 042ce5c1a6a4e040d6d6924f2d86e5534b730134 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:14:37 +0800 Subject: [PATCH 052/260] fix: ldap deps in `install-dependencies.sh` (#5385) --- t/plugin/ldap-auth.t | 2 +- utils/install-dependencies.sh | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/t/plugin/ldap-auth.t b/t/plugin/ldap-auth.t index 8232d9b5bd35..5674cb8b3413 100644 --- a/t/plugin/ldap-auth.t +++ b/t/plugin/ldap-auth.t @@ -308,4 +308,4 @@ find consumer user01 ) ngx.status = code } - } \ No newline at end of file + } diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh index e7cc5da0738c..85f9ff75cf7b 100755 --- a/utils/install-dependencies.sh +++ b/utils/install-dependencies.sh @@ -47,10 +47,9 @@ function install_dependencies_with_yum() { # add OpenResty source sudo yum install yum-utils sudo yum-config-manager --add-repo "https://openresty.org/package/${1}/openresty.repo" - sudo yum check-update # install OpenResty and some compilation tools - sudo yum install -y openresty curl git gcc openresty-openssl111-devel unzip pcre pcre-devel libldap2-dev + sudo yum install -y openresty curl git gcc openresty-openssl111-devel unzip pcre pcre-devel openldap-devel } # Install dependencies on ubuntu and debian From 957878fabad9179e60e8e2fc35582ade8f7b34ca Mon Sep 17 00:00:00 2001 From: arabot777 <30978207+arabot777@users.noreply.github.com> Date: Tue, 2 Nov 2021 19:32:58 +0800 Subject: [PATCH 053/260] docs: fix vars description about response-rewrite examples (#5403) Co-authored-by: liushan03 <liushan03@meicai.cn> --- docs/en/latest/plugins/response-rewrite.md | 2 +- docs/zh/latest/plugins/response-rewrite.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/plugins/response-rewrite.md b/docs/en/latest/plugins/response-rewrite.md index c03ac9ba6514..0ccd79f00746 100644 --- a/docs/en/latest/plugins/response-rewrite.md +++ b/docs/en/latest/plugins/response-rewrite.md @@ -67,7 +67,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "X-Server-balancer_addr": "$balancer_ip:$balancer_port" }, "vars":[ - [ "status","==","200" ] + [ "status","==",200 ] ] } }, diff --git a/docs/zh/latest/plugins/response-rewrite.md b/docs/zh/latest/plugins/response-rewrite.md index 36758d143e5a..d21ef559ed75 100644 --- a/docs/zh/latest/plugins/response-rewrite.md +++ b/docs/zh/latest/plugins/response-rewrite.md @@ -68,7 +68,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "X-Server-balancer_addr": "$balancer_ip:$balancer_port" }, "vars":[ - [ "status","==","200" ] + [ "status","==",200 ] ] } }, From 7b06fcc46874cd1ecb6a6ff33a29ccaed4bf8b29 Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Tue, 2 Nov 2021 20:38:28 +0800 Subject: [PATCH 054/260] feat(limit-conn): support multiple variables as key (#5354) --- apisix/plugins/limit-conn.lua | 7 +- apisix/plugins/limit-conn/init.lua | 25 ++++- docs/en/latest/plugins/limit-conn.md | 3 +- docs/zh/latest/plugins/limit-conn.md | 3 +- t/plugin/limit-conn2.t | 131 +++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 6 deletions(-) diff --git a/apisix/plugins/limit-conn.lua b/apisix/plugins/limit-conn.lua index d43737fbfc9f..d8389b701bcc 100644 --- a/apisix/plugins/limit-conn.lua +++ b/apisix/plugins/limit-conn.lua @@ -26,9 +26,10 @@ local schema = { burst = {type = "integer", minimum = 0}, default_conn_delay = {type = "number", exclusiveMinimum = 0}, only_use_default_delay = {type = "boolean", default = false}, - key = {type = "string", - enum = {"remote_addr", "server_addr", "http_x_real_ip", - "http_x_forwarded_for", "consumer_name"}, + key = {type = "string"}, + key_type = {type = "string", + enum = {"var", "var_combination"}, + default = "var", }, rejected_code = { type = "integer", minimum = 200, maximum = 599, default = 503 diff --git a/apisix/plugins/limit-conn/init.lua b/apisix/plugins/limit-conn/init.lua index 02b77f1fcc2a..adc1e6879c21 100644 --- a/apisix/plugins/limit-conn/init.lua +++ b/apisix/plugins/limit-conn/init.lua @@ -47,7 +47,30 @@ function _M.increase(conf, ctx) return 500 end - local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version + local conf_key = conf.key + local key + if conf.key_type == "var_combination" then + local err, n_resolved + key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var); + if err then + core.log.error("could not resolve vars in ", conf_key, " error: ", err) + end + + if n_resolved == 0 then + key = nil + end + else + key = ctx.var[conf_key] + end + + if key == nil then + core.log.info("bypass the limit conn as the key is empty") + -- Bypass the limit conn when the key is empty. + -- This behavior is the same as Nginx + return + end + + key = key .. ctx.conf_type .. ctx.conf_version core.log.info("limit key: ", key) local delay, err = lim:incoming(key, true) diff --git a/docs/en/latest/plugins/limit-conn.md b/docs/en/latest/plugins/limit-conn.md index dc01c74ede8d..1af26f3ca37c 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -41,7 +41,8 @@ Limiting request concurrency plugin. | burst | integer | required | | burst >= 0 | the number of excessive concurrent requests (or connections) allowed to be delayed. | | default_conn_delay | number | required | | default_conn_delay > 0 | the latency seconds of request when concurrent requests exceeding `conn` but below (`conn` + `burst`). | | only_use_default_delay | boolean | optional | false | [true,false] | enable the strict mode of the latency seconds. If you set this option to `true`, it will run strictly according to the latency seconds you set without additional calculation logic. | -| key | object | required | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | to limit the concurrency level. <br />For example, one can use the host name (or server zone) as the key so that we limit concurrency per host name. Otherwise, we can also use the client address as the key so that we can avoid a single client from flooding our service with too many parallel connections or requests. <br /> Now accept those as key: "remote_addr"(client's IP), "server_addr"(server's IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer's username). | +| key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | +| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr $consumer_name". | | rejected_code | string | optional | 503 | [200,...,599] | the HTTP status code returned when the request exceeds `conn` + `burst` will be rejected. | | rejected_msg | string | optional | | non-empty | the response body returned when the request exceeds `conn` + `burst` will be rejected. | | allow_degradation | boolean | optional | false | | Whether to enable plugin degradation when the limit-conn function is temporarily unavailable. Allow requests to continue when the value is set to true, default false. | diff --git a/docs/zh/latest/plugins/limit-conn.md b/docs/zh/latest/plugins/limit-conn.md index 5f66480dfe5f..0438229ad759 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -31,7 +31,8 @@ title: limit-conn | burst | integer | required | | burst >= 0 | 允许被延迟处理的并发请求数。 | | default_conn_delay | number | required | | default_conn_delay > 0 | 默认的典型连接(或请求)的处理延迟时间。 | | only_use_default_delay | boolean | optional | false | [true,false] | 延迟时间的严格模式。 如果设置为`true`的话,将会严格按照设置的时间来进行延迟 | -| key | object | required | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | 用户指定的限制并发级别的关键字,可以是客户端 IP 或服务端 IP。<br />例如,可以使用主机名(或服务器区域)作为关键字,以便限制每个主机名的并发性。 否则,我们也可以使用客户端地址作为关键字,这样我们就可以避免单个客户端用太多的并行连接或请求淹没我们的服务。 <br />当前接受的 key 有:"remote_addr"(客户端 IP 地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP", "consumer_name"(consumer 的 username)。 | +| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | +| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。 | | rejected_code | string | optional | 503 | [200,...,599] | 当请求超过 `conn` + `burst` 这个阈值时,返回的 HTTP 状态码 | | rejected_msg | string | 可选 | | 非空 | 当请求超过 `conn` + `burst` 这个阈值时,返回的响应体。 | | allow_degradation | boolean | 可选 | false | | 当插件功能临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| diff --git a/t/plugin/limit-conn2.t b/t/plugin/limit-conn2.t index 8eec44d9bdb6..187e3aa74b5a 100644 --- a/t/plugin/limit-conn2.t +++ b/t/plugin/limit-conn2.t @@ -308,3 +308,134 @@ request latency is nil --- error_code: 400 --- response_body {"error_msg":"failed to check the configuration of plugin limit-conn err: property \"rejected_msg\" validation failed: string too short, expected at least 1, got 0"} + + + +=== TEST 9: set key type to var_combination +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-conn": { + "conn": 2, + "burst": 1, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key": "$http_a $http_b", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/limit_conn" + }]], + [[{ + "node": { + "value": { + "plugins": { + "limit-conn": { + "conn": 2, + "burst": 1, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key": "$http_a $http_b", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/limit_conn" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 10: Don't exceed the burst +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/limit_conn" + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {a = i}}) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- timeout: 10s +--- response_body +[200,200] +--- no_error_log +[error] + + + +=== TEST 11: bypass empty key +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/limit_conn" + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200] +--- error_log +bypass the limit conn as the key is empty From a0efeca194bcca58e62d73c0a19464f480a1b7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Tue, 2 Nov 2021 21:49:41 +0800 Subject: [PATCH 055/260] test: show error.log once the CLI test failed (#5405) --- .github/workflows/fuzzing-ci.yaml | 1 + ci/centos7-ci.sh | 2 +- ci/common.sh | 1 + ci/linux_apisix_current_luarocks_runner.sh | 2 ++ ci/linux_apisix_master_luarocks_runner.sh | 2 ++ t/cli/common.sh | 7 +++++++ 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index 00edfd09201c..ec9dfa84e362 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -52,6 +52,7 @@ jobs: sudo apt-get install -y git openresty curl openresty-openssl111-dev unzip make gcc libldap2-dev ./utils/linux-install-luarocks.sh + git config --global url.https://github.com/.insteadOf git://github.com/ make deps make init make run diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index 29c7c36d62f2..f5a17996dc6f 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -74,7 +74,7 @@ install_dependencies() { # install dependencies git clone https://github.com/iresty/test-nginx.git test-nginx - make deps + create_lua_deps } run_case() { diff --git a/ci/common.sh b/ci/common.sh index 0aae0703c49f..f27583b3b495 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -26,6 +26,7 @@ export_or_prefix() { create_lua_deps() { echo "Create lua deps" + git config --global url.https://github.com/.insteadOf git://github.com/ make deps # maybe reopen this feature later # luarocks install luacov-coveralls --tree=deps --local > build.log 2>&1 || (cat build.log && exit 1) diff --git a/ci/linux_apisix_current_luarocks_runner.sh b/ci/linux_apisix_current_luarocks_runner.sh index a143d479d0a2..2682a4fca3dc 100755 --- a/ci/linux_apisix_current_luarocks_runner.sh +++ b/ci/linux_apisix_current_luarocks_runner.sh @@ -32,6 +32,8 @@ script() { sudo rm -rf /usr/local/apisix + git config --global url.https://github.com/.insteadOf git://github.com/ + # install APISIX with local version sudo luarocks install rockspec/apisix-master-0.rockspec --only-deps > build.log 2>&1 || (cat build.log && exit 1) sudo luarocks make rockspec/apisix-master-0.rockspec > build.log 2>&1 || (cat build.log && exit 1) diff --git a/ci/linux_apisix_master_luarocks_runner.sh b/ci/linux_apisix_master_luarocks_runner.sh index a75fdf6c2488..ed3e58c46b9b 100755 --- a/ci/linux_apisix_master_luarocks_runner.sh +++ b/ci/linux_apisix_master_luarocks_runner.sh @@ -36,6 +36,8 @@ script() { mkdir tmp && cd tmp cp -r ../utils ./ + git config --global url.https://github.com/.insteadOf git://github.com/ + # install APISIX by luarocks sudo luarocks install $APISIX_MAIN > build.log 2>&1 || (cat build.log && exit 1) cp ../bin/apisix /usr/local/bin/apisix diff --git a/t/cli/common.sh b/t/cli/common.sh index 521d41d8e6c6..9da89a022330 100644 --- a/t/cli/common.sh +++ b/t/cli/common.sh @@ -21,7 +21,14 @@ set -ex +check_failure() { + cat logs/error.log +} + clean_up() { + if [ $? -gt 0 ]; then + check_failure + fi make stop || true git checkout conf/config.yaml } From 5ea64045d5ed6083d6d730861475f1b1e708cd9b Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Wed, 3 Nov 2021 12:34:35 +0800 Subject: [PATCH 056/260] docs: fix markdown render issue in the example (#5390) --- docs/en/latest/plugins/limit-req.md | 2 +- docs/zh/latest/plugins/limit-req.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index 166fa1bf9710..500ed8df1976 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -41,7 +41,7 @@ limit request rate using the "leaky bucket" method. | rate | integer | required | | rate > 0 | the specified request rate (number per second) threshold. Requests exceeding this rate (and below `burst`) will get delayed to conform to the rate. | | burst | integer | required | | burst >= 0 | the number of excessive requests per second allowed to be delayed. Requests exceeding this hard limit will get rejected immediately. | | key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | -| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr|$consumer_name". | +| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr $consumer_name". | | rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected. | | rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. | | nodelay | boolean | optional | false | | If nodelay flag is true, bursted requests will not get delayed | diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index fb7b7d5af8e3..e1ba284f242d 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -41,7 +41,7 @@ title: limit-req | rate | integer | 必须 | | rate > 0 | 指定的请求速率(以秒为单位),请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求会被加上延时。 | | burst | integer | 必须 | | burst >= 0 | 请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 | | key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | -| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr|$consumer_name"。 | +| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码。 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | | nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求不会加上延迟, 如果是 false,则会加上延迟。 | From 9ba76cc265f24751bdbe6fad9bd8ea3c19d8f59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Wed, 3 Nov 2021 12:50:00 +0800 Subject: [PATCH 057/260] test: avoid client timeout caused by DNS query (#5401) --- t/stream-plugin/mqtt-proxy.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/stream-plugin/mqtt-proxy.t b/t/stream-plugin/mqtt-proxy.t index 5e74823bd87f..0aa879920603 100644 --- a/t/stream-plugin/mqtt-proxy.t +++ b/t/stream-plugin/mqtt-proxy.t @@ -264,3 +264,4 @@ passed "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- error_log failed to parse domain: loc, error: +--- timeout: 10 From 25ee29d41b4310d8c2168a74025ada3ffd9aab9e Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Wed, 3 Nov 2021 12:50:17 +0800 Subject: [PATCH 058/260] ci: lint for editorconfig (#5391) --- .editorconfig | 47 ++++++++++++ .github/actions/.editorconfig | 19 +++++ .github/workflows/lint.yml | 21 ++++++ apisix/plugins/zipkin/random_sampler.lua | 3 +- conf/config-default.yaml | 94 ++++++++++++------------ t/admin/routes4.t | 2 +- t/admin/services.t | 2 +- t/admin/upstream4.t | 4 +- t/grpc_server_example | 2 +- t/plugin/consumer-restriction.t | 4 +- t/plugin/error-log-logger.t | 6 +- t/plugin/prometheus2.t | 30 ++++---- t/toolkit | 2 +- utils/install_yaml_conf.sh | 2 +- 14 files changed, 162 insertions(+), 76 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/actions/.editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..0096ce7b2d1f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,47 @@ +# +# 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. +# + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[.gitmodules] +indent_style = tab + +[Makefile] +indent_style = tab + +[*.{yml,yaml}] +indent_size = 2 + +[*.go] +indent_style = tab +## ignore ASF license +block_comment_start = /* +block_comment = * +block_comment_end = */ + +[**go.mod] +indent_style = tab + +[t/coredns/db.test.local] +indent_style = unset diff --git a/.github/actions/.editorconfig b/.github/actions/.editorconfig new file mode 100644 index 000000000000..94f13f35bd9b --- /dev/null +++ b/.github/actions/.editorconfig @@ -0,0 +1,19 @@ +# +# 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. +# + +# ignore third-party actions +root = true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f4ed245e6416..5995b86221db 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -29,3 +29,24 @@ jobs: - name: Plugin Code run: | sudo bash ./utils/check-plugins-code.sh + + ci-eclint: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check out code + uses: actions/checkout@v2.3.5 + + - name: Setup Nodejs env + uses: actions/setup-node@v2 + with: + node-version: '12' + + - name: Install eclint + run: | + sudo npm install -g eclint + + - name: Run eclint + run: | + eclint check diff --git a/apisix/plugins/zipkin/random_sampler.lua b/apisix/plugins/zipkin/random_sampler.lua index cfed3f914b07..d458bcec73a2 100644 --- a/apisix/plugins/zipkin/random_sampler.lua +++ b/apisix/plugins/zipkin/random_sampler.lua @@ -24,8 +24,7 @@ local _M = {} local mt = { __index = _M } function _M.new(conf) - return setmetatable({ - }, mt) + return setmetatable({}, mt) end function _M.sample(self, sample_ratio) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 54e760d9b48b..236d50bc4cf0 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -20,48 +20,48 @@ # apisix: - # node_listen: 9080 # APISIX listening port - node_listen: # This style support multiple ports + # node_listen: 9080 # APISIX listening port + node_listen: # This style support multiple ports - 9080 # - port: 9081 - # enable_http2: true # If not set, the default value is `false`. - # - ip: 127.0.0.2 # Specific IP, If not set, the default value is `0.0.0.0`. + # enable_http2: true # If not set, the default value is `false`. + # - ip: 127.0.0.2 # Specific IP, If not set, the default value is `0.0.0.0`. # port: 9082 # enable_http2: true enable_admin: true - enable_admin_cors: true # Admin API support CORS response headers. - enable_dev_mode: false # Sets nginx worker_processes to 1 if set to true - enable_reuseport: true # Enable nginx SO_REUSEPORT switch if set to true. + enable_admin_cors: true # Admin API support CORS response headers. + enable_dev_mode: false # Sets nginx worker_processes to 1 if set to true + enable_reuseport: true # Enable nginx SO_REUSEPORT switch if set to true. enable_ipv6: true - config_center: etcd # etcd: use etcd to store the config value - # yaml: fetch the config value from local yaml file `/your_path/conf/apisix.yaml` + config_center: etcd # etcd: use etcd to store the config value + # yaml: fetch the config value from local yaml file `/your_path/conf/apisix.yaml` - #proxy_protocol: # Proxy Protocol configuration - #listen_http_port: 9181 # The port with proxy protocol for http, it differs from node_listen and port_admin. - # This port can only receive http request with proxy protocol, but node_listen & port_admin - # can only receive http request. If you enable proxy protocol, you must use this port to - # receive http request with proxy protocol - #listen_https_port: 9182 # The port with proxy protocol for https - #enable_tcp_pp: true # Enable the proxy protocol for tcp proxy, it works for stream_proxy.tcp option - #enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the upstream server - enable_server_tokens: true # Whether the APISIX version number should be shown in Server header. - # It's enabled by default. + #proxy_protocol: # Proxy Protocol configuration + #listen_http_port: 9181 # The port with proxy protocol for http, it differs from node_listen and port_admin. + # This port can only receive http request with proxy protocol, but node_listen & port_admin + # can only receive http request. If you enable proxy protocol, you must use this port to + # receive http request with proxy protocol + #listen_https_port: 9182 # The port with proxy protocol for https + #enable_tcp_pp: true # Enable the proxy protocol for tcp proxy, it works for stream_proxy.tcp option + #enable_tcp_pp_to_upstream: true # Enables the proxy protocol to the upstream server + enable_server_tokens: true # Whether the APISIX version number should be shown in Server header. + # It's enabled by default. # configurations to load third party code and/or override the builtin one. - extra_lua_path: "" # extend lua_package_path to load third party code - extra_lua_cpath: "" # extend lua_package_cpath to load third party code + extra_lua_path: "" # extend lua_package_path to load third party code + extra_lua_cpath: "" # extend lua_package_cpath to load third party code #lua_module_hook: "my_project.my_hook" # the hook module which will be used to inject third party code into APISIX - proxy_cache: # Proxy Caching configuration - cache_ttl: 10s # The default caching time in disk if the upstream does not specify the cache time - zones: # The parameters of a cache - - name: disk_cache_one # The name of the cache, administrator can specify - # which cache to use by name in the admin api (disk|memory) - memory_size: 50m # The size of shared memory, it's used to store the cache index for - # disk strategy, store cache content for memory strategy (disk|memory) - disk_size: 1G # The size of disk, it's used to store the cache data (disk) + proxy_cache: # Proxy Caching configuration + cache_ttl: 10s # The default caching time in disk if the upstream does not specify the cache time + zones: # The parameters of a cache + - name: disk_cache_one # The name of the cache, administrator can specify + # which cache to use by name in the admin api (disk|memory) + memory_size: 50m # The size of shared memory, it's used to store the cache index for + # disk strategy, store cache content for memory strategy (disk|memory) + disk_size: 1G # The size of disk, it's used to store the cache data (disk) disk_path: /tmp/disk_cache_one # The path to store the cache data (disk) - cache_levels: 1:2 # The hierarchy levels of a cache (disk) + cache_levels: 1:2 # The hierarchy levels of a cache (disk) #- name: disk_cache_two # memory_size: 50m # disk_size: 1G @@ -100,8 +100,8 @@ apisix: role: viewer delete_uri_tail_slash: false # delete the '/' at the end of the URI - global_rule_skip_internal_api: true # does not run global rule in internal apis - # api that path starts with "/apisix" is considered to be internal api + global_rule_skip_internal_api: true # does not run global rule in internal apis + # api that path starts with "/apisix" is considered to be internal api router: http: radixtree_uri # radixtree_uri: match route by uri(base on radixtree) # radixtree_host_uri: match route by host + uri(base on radixtree) @@ -202,16 +202,16 @@ nginx_config: # config for render the template to generate n # The configuration should be well indented! http: - enable_access_log: true # enable access log or not, default true + enable_access_log: true # enable access log or not, default true access_log: logs/access.log access_log_format: "$remote_addr - $remote_user [$time_local] $http_host \"$request\" $status $body_bytes_sent $request_time \"$http_referer\" \"$http_user_agent\" $upstream_addr $upstream_status $upstream_response_time \"$upstream_scheme://$upstream_host$upstream_uri\"" access_log_format_escape: default # allows setting json or default characters escaping in variables - keepalive_timeout: 60s # timeout during which a keep-alive client connection will stay open on the server side. - client_header_timeout: 60s # timeout for reading client request header, then 408 (Request Time-out) error is returned to the client - client_body_timeout: 60s # timeout for reading client request body, then 408 (Request Time-out) error is returned to the client - client_max_body_size: 0 # The maximum allowed size of the client request body. - # If exceeded, the 413 (Request Entity Too Large) error is returned to the client. - # Note that unlike Nginx, we don't limit the body size by default. + keepalive_timeout: 60s # timeout during which a keep-alive client connection will stay open on the server side. + client_header_timeout: 60s # timeout for reading client request header, then 408 (Request Time-out) error is returned to the client + client_body_timeout: 60s # timeout for reading client request body, then 408 (Request Time-out) error is returned to the client + client_max_body_size: 0 # The maximum allowed size of the client request body. + # If exceeded, the 413 (Request Entity Too Large) error is returned to the client. + # Note that unlike Nginx, we don't limit the body size by default. send_timeout: 10s # timeout for transmitting a response to the client.then the connection is closed underscores_in_headers: "on" # default enables the use of underscores in client request header fields @@ -227,14 +227,14 @@ nginx_config: # config for render the template to generate n # when establishing a connection with the proxied HTTPS server. proxy_ssl_server_name: true upstream: - keepalive: 320 # Sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process. - # When this number is exceeded, the least recently used connections are closed. - keepalive_requests: 1000 # Sets the maximum number of requests that can be served through one keepalive connection. - # After the maximum number of requests is made, the connection is closed. - keepalive_timeout: 60s # Sets a timeout during which an idle keepalive connection to an upstream server will stay open. - charset: utf-8 # Adds the specified charset to the "Content-Type" response header field, see - # http://nginx.org/en/docs/http/ngx_http_charset_module.html#charset - variables_hash_max_size: 2048 # Sets the maximum size of the variables hash table. + keepalive: 320 # Sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process. + # When this number is exceeded, the least recently used connections are closed. + keepalive_requests: 1000 # Sets the maximum number of requests that can be served through one keepalive connection. + # After the maximum number of requests is made, the connection is closed. + keepalive_timeout: 60s # Sets a timeout during which an idle keepalive connection to an upstream server will stay open. + charset: utf-8 # Adds the specified charset to the "Content-Type" response header field, see + # http://nginx.org/en/docs/http/ngx_http_charset_module.html#charset + variables_hash_max_size: 2048 # Sets the maximum size of the variables hash table. lua_shared_dict: internal-status: 10m diff --git a/t/admin/routes4.t b/t/admin/routes4.t index e425df3116c7..158d6ad6bdc8 100644 --- a/t/admin/routes4.t +++ b/t/admin/routes4.t @@ -754,7 +754,7 @@ passed "methods": ["GET"], "uri": "/index.html", "labels": { - "env": ["production", "release"] + "env": ["production", "release"] } }]] ) diff --git a/t/admin/services.t b/t/admin/services.t index dcb7c7aed9d5..8493b5177c97 100644 --- a/t/admin/services.t +++ b/t/admin/services.t @@ -1220,7 +1220,7 @@ passed ngx.HTTP_PATCH, [[{ "labels": { - "build": "17" + "build": "17" } }]], [[{ diff --git a/t/admin/upstream4.t b/t/admin/upstream4.t index f3078a1ddc39..c829fd2826c0 100644 --- a/t/admin/upstream4.t +++ b/t/admin/upstream4.t @@ -344,7 +344,7 @@ passed ngx.HTTP_PATCH, [[{ "labels": { - "build": "17" + "build": "17" } }]], [[{ @@ -388,7 +388,7 @@ passed }, "type": "roundrobin", "labels": { - "env": ["production", "release"] + "env": ["production", "release"] } }]] ) diff --git a/t/grpc_server_example b/t/grpc_server_example index 2df0e72ebacf..f7ee318f701e 160000 --- a/t/grpc_server_example +++ b/t/grpc_server_example @@ -1 +1 @@ -Subproject commit 2df0e72ebacfb306fcfb3d61f27583017d0c3c10 +Subproject commit f7ee318f701e04bf21bf000baab539f5a8bc7eaa diff --git a/t/plugin/consumer-restriction.t b/t/plugin/consumer-restriction.t index c8d954a9d6fa..21c26ccdbbcd 100644 --- a/t/plugin/consumer-restriction.t +++ b/t/plugin/consumer-restriction.t @@ -31,8 +31,8 @@ __DATA__ content_by_lua_block { local plugin = require("apisix.plugins.consumer-restriction") local conf = { - title = "whitelist", - whitelist = { + title = "whitelist", + whitelist = { "jack1", "jack2" } diff --git a/t/plugin/error-log-logger.t b/t/plugin/error-log-logger.t index 9dd1c139f738..7aa37a422b16 100644 --- a/t/plugin/error-log-logger.t +++ b/t/plugin/error-log-logger.t @@ -57,9 +57,9 @@ _EOC_ $block->set_value("stream_config", $stream_single_server); my $stream_default_server = <<_EOC_; - content_by_lua_block { - ngx.log(ngx.INFO, "a stream server") - } + content_by_lua_block { + ngx.log(ngx.INFO, "a stream server") + } _EOC_ $block->set_value("stream_server_config", $stream_default_server); diff --git a/t/plugin/prometheus2.t b/t/plugin/prometheus2.t index 5749f2f52c97..25fa21e8962d 100644 --- a/t/plugin/prometheus2.t +++ b/t/plugin/prometheus2.t @@ -354,21 +354,21 @@ GET /apisix/prometheus/metrics "plugins": { "prometheus": {}, "syslog": { - "host": "127.0.0.1", - "include_req_body": false, - "max_retry_times": 1, - "tls": false, - "retry_interval": 1, - "batch_max_size": 1000, - "buffer_duration": 60, - "port": 1000, - "name": "sys-logger", - "flush_limit": 4096, - "sock_type": "tcp", - "timeout": 3, - "drop_limit": 1048576, - "pool_size": 5 - } + "host": "127.0.0.1", + "include_req_body": false, + "max_retry_times": 1, + "tls": false, + "retry_interval": 1, + "batch_max_size": 1000, + "buffer_duration": 60, + "port": 1000, + "name": "sys-logger", + "flush_limit": 4096, + "sock_type": "tcp", + "timeout": 3, + "drop_limit": 1048576, + "pool_size": 5 + } }, "upstream": { "nodes": { diff --git a/t/toolkit b/t/toolkit index ab2471cc9cbe..94f471d3a904 160000 --- a/t/toolkit +++ b/t/toolkit @@ -1 +1 @@ -Subproject commit ab2471cc9cbeec6fe605120160eeb9dd17ddda2c +Subproject commit 94f471d3a9045667bc1f1784a21f89315cd64f7e diff --git a/utils/install_yaml_conf.sh b/utils/install_yaml_conf.sh index 879aa51ed83b..c369487f90e4 100755 --- a/utils/install_yaml_conf.sh +++ b/utils/install_yaml_conf.sh @@ -21,5 +21,5 @@ target_file=$1 if [ ! -f "$target_file" ]; then cp ./conf/config.yaml $target_file - chmod 644 $target_file + chmod 644 $target_file fi From f505d99e97942defd5fa8280d8fe5caabb1e1d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Wed, 3 Nov 2021 15:26:48 +0800 Subject: [PATCH 059/260] test: remove unused conf & make t/core/config_etcd.t stable (#5407) --- t/core/config_etcd.t | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/t/core/config_etcd.t b/t/core/config_etcd.t index f5a377dea7a9..1e98434b62c4 100644 --- a/t/core/config_etcd.t +++ b/t/core/config_etcd.t @@ -278,15 +278,6 @@ etcd auth failed === TEST 8: ensure add prefix automatically for _M.getkey -apisix: - node_listen: 1984 - admin_key: null -etcd: - host: - - "http://127.0.0.1:2379" - tls: - verify: false - prefix: "/apisix" --- config location /t { content_by_lua_block { @@ -320,15 +311,6 @@ passed === TEST 9: Test ETCD health check mode switch during APISIX startup -apisix: - node_listen: 1984 - admin_key: null -etcd: - host: - - "http://127.0.0.1:2379" - tls: - verify: false - prefix: "/apisix" --- config location /t { content_by_lua_block { @@ -341,7 +323,6 @@ GET /t passed --- grep_error_log eval qr/healthy check use \S+ \w+/ ---- grep_error_log_out -healthy check use round robin -healthy check use ngx.shared dict -healthy check use ngx.shared dict +--- grep_error_log_out eval +qr/healthy check use round robin +(healthy check use ngx.shared dict){1,}/ From fc5f74709b44f6508ab89433e13b7613908fac5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Wed, 3 Nov 2021 15:45:55 +0800 Subject: [PATCH 060/260] ci: no need to exclude submodule explicitly (#5393) --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5995b86221db..e453e7cce784 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,10 +22,10 @@ jobs: wget -O - -q https://git.io/misspell | sh -s -- -b . - name: Misspell run: | - git ls-files | grep -v "docs/es" | grep -v "t/toolkit" | xargs ./misspell -error + git grep --cached -l '' | xargs ./misspell -error - name: Merge conflict run: | - grep "^<<<<<<< HEAD" $(git ls-files | grep -v "t/toolkit" | xargs) && exit 1 || true + grep "^<<<<<<< HEAD" $(git grep --cached -l '' | xargs) && exit 1 || true - name: Plugin Code run: | sudo bash ./utils/check-plugins-code.sh From 10833592282787352ee9960ca1c5593985b785bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 17:26:59 +0800 Subject: [PATCH 061/260] chore(deps): bump actions/checkout from 2.3.5 to 2.4.0 (#5408) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/centos7-ci.yml | 2 +- .github/workflows/chaos.yml | 2 +- .github/workflows/cli.yml | 2 +- .github/workflows/code-lint.yml | 2 +- .github/workflows/doc-lint.yml | 2 +- .github/workflows/fuzzing-ci.yaml | 2 +- .github/workflows/license-checker.yml | 2 +- .github/workflows/lint.yml | 6 +++--- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1dadc1b5372..66e0c05100c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 with: submodules: recursive diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index c63d27462888..00f1125e25a9 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 with: submodules: recursive diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 13cbc3ce698f..d4b82f0f5d40 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 35 steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 with: submodules: recursive diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index e5899db1b723..c8d749501683 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 with: submodules: recursive diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index 7cd660afd3f9..4517c7985914 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 - name: Install run: | . ./ci/common.sh diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 8f7e58f6d7bb..2bc2a9eb99c4 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 1 steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 - name: 🚀 Use Node.js uses: actions/setup-node@v2.4.1 with: diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index ec9dfa84e362..154b83cd12db 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -26,7 +26,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 with: submodules: recursive diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index ad2e71c7f052..935552c657da 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -32,7 +32,7 @@ jobs: timeout-minutes: 3 steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 - name: Check License Header uses: apache/skywalking-eyes@v0.2.0 env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e453e7cce784..1e4c9bb2a0e3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 1 steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 - name: 🧹 Check for trailing whitespace run: "! git grep -EIn $'[ \t]+$'" misc: @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code. - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Install run: | wget -O - -q https://git.io/misspell | sh -s -- -b . @@ -36,7 +36,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2.3.5 + uses: actions/checkout@v2.4.0 - name: Setup Nodejs env uses: actions/setup-node@v2 From 4dafab5afa3293b3d72007517246e01da385f8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Thu, 4 Nov 2021 09:48:02 +0800 Subject: [PATCH 062/260] feat(limit-conn): support multiple variables as key in L4 (#5413) --- apisix/stream/plugins/limit-conn.lua | 7 +- docs/en/latest/plugins/limit-conn.md | 4 - docs/en/latest/plugins/limit-req.md | 2 - docs/zh/latest/plugins/limit-conn.md | 4 - docs/zh/latest/plugins/limit-req.md | 2 - t/admin/plugins.t | 2 +- t/stream-plugin/limit-conn.t | 145 +++++++++++++++++++++++++++ 7 files changed, 150 insertions(+), 16 deletions(-) diff --git a/apisix/stream/plugins/limit-conn.lua b/apisix/stream/plugins/limit-conn.lua index d2bd25e97d57..1beb7c7a1f40 100644 --- a/apisix/stream/plugins/limit-conn.lua +++ b/apisix/stream/plugins/limit-conn.lua @@ -26,9 +26,10 @@ local schema = { burst = {type = "integer", minimum = 0}, default_conn_delay = {type = "number", exclusiveMinimum = 0}, only_use_default_delay = {type = "boolean", default = false}, - key = { - type = "string", - enum = {"remote_addr", "server_addr"} + key = {type = "string"}, + key_type = {type = "string", + enum = {"var", "var_combination"}, + default = "var", }, }, required = {"conn", "burst", "default_conn_delay", "key"} diff --git a/docs/en/latest/plugins/limit-conn.md b/docs/en/latest/plugins/limit-conn.md index 1af26f3ca37c..479003916565 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -47,10 +47,6 @@ Limiting request concurrency plugin. | rejected_msg | string | optional | | non-empty | the response body returned when the request exceeds `conn` + `burst` will be rejected. | | allow_degradation | boolean | optional | false | | Whether to enable plugin degradation when the limit-conn function is temporarily unavailable. Allow requests to continue when the value is set to true, default false. | -**Key can be customized by the user, only need to modify a line of code of the plug-in to complete. It is a security consideration that is not open in the plugin.** - -When used in the stream proxy, only `remote_addr` and `server_addr` can be used as key. And `rejected_code` is meaningless. - ## How To Enable Here's an example, enable the limit-conn plugin on the specified route: diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index 500ed8df1976..0c16c766ecc0 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -47,8 +47,6 @@ limit request rate using the "leaky bucket" method. | nodelay | boolean | optional | false | | If nodelay flag is true, bursted requests will not get delayed | | allow_degradation | boolean | optional | false | | Whether to enable plugin degradation when the limit-req function is temporarily unavailable. Allow requests to continue when the value is set to true, default false. | -**Key can be customized by the user, only need to modify a line of code of the plug-in to complete. It is a security consideration that is not open in the plugin.** - ## Example ### How to enable on the `route` or `service` diff --git a/docs/zh/latest/plugins/limit-conn.md b/docs/zh/latest/plugins/limit-conn.md index 0438229ad759..ca3a381fcc3b 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -37,10 +37,6 @@ title: limit-conn | rejected_msg | string | 可选 | | 非空 | 当请求超过 `conn` + `burst` 这个阈值时,返回的响应体。 | | allow_degradation | boolean | 可选 | false | | 当插件功能临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| -**注:key 是可以被用户自定义的,只需要修改插件的一行代码即可完成。并没有在插件中放开是处于安全的考虑。** - -在 stream 代理中使用该插件时,只有 `remote_addr` 和 `server_addr` 可以被用作 key。另外设置 `rejected_code` 毫无意义。 - #### 如何启用 下面是一个示例,在指定的 route 上开启了 limit-conn 插件: diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index e1ba284f242d..d0e8dd9c9e34 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -47,8 +47,6 @@ title: limit-req | nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求不会加上延迟, 如果是 false,则会加上延迟。 | | allow_degradation | boolean | 可选 | false | | 当限速插件功能临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| -**key 是可以被用户自定义的,只需要修改插件的一行代码即可完成。并没有在插件中放开是处于安全的考虑。** - ## 示例 ### 如何在`route`或`service`上使用 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index b5196dd0a042..2c67ef368228 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -298,7 +298,7 @@ qr/\{"properties":\{"password":\{"type":"string"\},"username":\{"type":"string"\ } } --- response_body -{"priority":1003,"schema":{"$comment":"this is a mark for our injected plugin schema","properties":{"burst":{"minimum":0,"type":"integer"},"conn":{"exclusiveMinimum":0,"type":"integer"},"default_conn_delay":{"exclusiveMinimum":0,"type":"number"},"disable":{"type":"boolean"},"key":{"enum":["remote_addr","server_addr"],"type":"string"},"only_use_default_delay":{"default":false,"type":"boolean"}},"required":["conn","burst","default_conn_delay","key"],"type":"object"},"version":0.1} +{"priority":1003,"schema":{"$comment":"this is a mark for our injected plugin schema","properties":{"burst":{"minimum":0,"type":"integer"},"conn":{"exclusiveMinimum":0,"type":"integer"},"default_conn_delay":{"exclusiveMinimum":0,"type":"number"},"disable":{"type":"boolean"},"key":{"type":"string"},"key_type":{"default":"var","enum":["var","var_combination"],"type":"string"},"only_use_default_delay":{"default":false,"type":"boolean"}},"required":["conn","burst","default_conn_delay","key"],"type":"object"},"version":0.1} --- no_error_log [error] diff --git a/t/stream-plugin/limit-conn.t b/t/stream-plugin/limit-conn.t index 95e0b827a39b..c166aacb674f 100644 --- a/t/stream-plugin/limit-conn.t +++ b/t/stream-plugin/limit-conn.t @@ -189,3 +189,148 @@ GET /test_concurrency --- error_log Connection reset by peer --- stream_enable + + + +=== TEST 5: var combination +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/stream_routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-conn": { + "conn": 2, + "burst": 1, + "default_conn_delay": 0.1, + "key": "$remote_addr $server_addr", + "key_type": "var_combination" + } + }, + "upstream_id": "1" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: exceeding the burst +--- request +GET /test_concurrency +--- response_body +200 +200 +200 +503 +503 +--- error_log +Connection reset by peer +--- stream_enable + + + +=== TEST 7: var combination (not exceed the burst) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/stream_routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-conn": { + "conn": 2, + "burst": 1, + "default_conn_delay": 0.1, + "key": "$remote_port $server_addr", + "key_type": "var_combination" + } + }, + "upstream_id": "1" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 8: hit +--- request +GET /test_concurrency +--- response_body +200 +200 +200 +200 +200 +--- stream_enable + + + +=== TEST 9: bypass empty key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/stream_routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-conn": { + "conn": 2, + "burst": 1, + "default_conn_delay": 0.1, + "key": "$proxy_protocol_addr $proxy_protocol_port", + "key_type": "var_combination" + } + }, + "upstream_id": "1" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 10: hit +--- request +GET /test_concurrency +--- response_body +200 +200 +200 +200 +200 +--- error_log +bypass the limit conn as the key is empty +--- stream_enable From 4346782a878d5525310a2919f847e7249ad25348 Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Thu, 4 Nov 2021 15:51:37 +0800 Subject: [PATCH 063/260] feat(limit-count): support multiple variables as key (#5378) --- apisix/plugins/limit-count.lua | 33 +++- docs/en/latest/plugins/limit-count.md | 34 +++- docs/zh/latest/plugins/limit-count.md | 31 +++- t/control/services.t | 2 +- t/plugin/limit-count-redis-cluster.t | 6 +- t/plugin/limit-count.t | 5 +- t/plugin/limit-count2.t | 236 ++++++++++++++++++++++++++ 7 files changed, 325 insertions(+), 22 deletions(-) diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua index d2831c36a281..1a52ef4e73b8 100644 --- a/apisix/plugins/limit-count.lua +++ b/apisix/plugins/limit-count.lua @@ -36,11 +36,10 @@ local schema = { properties = { count = {type = "integer", exclusiveMinimum = 0}, time_window = {type = "integer", exclusiveMinimum = 0}, - key = { - type = "string", - enum = {"remote_addr", "server_addr", "http_x_real_ip", - "http_x_forwarded_for", "consumer_name", "service_id"}, - default = "remote_addr", + key = {type = "string", default = "remote_addr"}, + key_type = {type = "string", + enum = {"var", "var_combination"}, + default = "var", }, rejected_code = { type = "integer", minimum = 200, maximum = 599, default = 503 @@ -171,7 +170,29 @@ function _M.access(conf, ctx) return 500 end - local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version + local conf_key = conf.key + local key + if conf.key_type == "var_combination" then + local err, n_resolved + key, err, n_resolved = core.utils.resolve_var(conf_key, ctx.var); + if err then + core.log.error("could not resolve vars in ", conf_key, " error: ", err) + end + + if n_resolved == 0 then + key = nil + end + else + key = ctx.var[conf_key] + end + + if key == nil then + core.log.info("bypass the limit count as the key is empty") + -- Bypass the limit count when the key is empty. + -- This behavior is the same as Nginx + return + end + key = key .. ctx.conf_type .. ctx.conf_version core.log.info("limit key: ", key) local delay, remaining = lim:incoming(key, true) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 2a4302e61a41..2c267603f0bf 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -39,7 +39,8 @@ Limit request rate by a fixed number of requests in a given time window. | ------------------- | ------- | --------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | count | integer | required | | count > 0 | the specified number of requests threshold. | | time_window | integer | required | | time_window > 0 | the time window in seconds before the request count is reset. | -| key | string | optional | "remote_addr" | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name", "service_id"] | The user specified key to limit the count. <br /> Now accept those as key: "remote_addr"(client's IP), "server_addr"(server's IP), "X-Forwarded-For/X-Real-IP" in request header, "consumer_name"(consumer's username) and "service_id". | +| key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | +| key | string | optional | "remote_addr" | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable. If the `key_type` is "var_combination", the key will be a combination of variables. For example, if we use "$remote_addr $consumer_name" as keys, plugin will be restricted by two keys which are "remote_addr" and "consumer_name". | | rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected, default 503. | | rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. | | policy | string | optional | "local" | ["local", "redis", "redis-cluster"] | The rate-limiting policies to use for retrieving and incrementing the limits. Available values are `local`(the counters will be stored locally in-memory on the node), `redis`(counters are stored on a Redis server and will be shared across the nodes, usually use it to do the global speed limit), and `redis-cluster` which works the same as `redis` but with redis cluster. | @@ -53,11 +54,9 @@ Limit request rate by a fixed number of requests in a given time window. | redis_cluster_nodes | array | required when policy is `redis-cluster` | | | When using `redis-cluster` policy,This property is a list of addresses of Redis cluster service nodes (at least two). | | redis_cluster_name | string | required when policy is `redis-cluster` | | | When using `redis-cluster` policy, this property is the name of Redis cluster service nodes. | -**Key can be customized by the user, only need to modify a line of code of the plug-in to complete. It is a security consideration that is not open in the plugin.** - ## How To Enable -Here's an example, enable the `limit count` plugin on the specified route: +Here's an example, enable the `limit count` plugin on the specified route when setting `key_type` to `var` : ```shell curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -68,13 +67,38 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "count": 2, "time_window": 60, "rejected_code": 503, + "key_type": "var", "key": "remote_addr" } }, "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:9001": 1 + } + } +}' +``` + +Here's an example, enable the `limit count` plugin on the specified route when setting `key_type` to `var_combination` : + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key_type": "var_combination", + "key": "$consumer_name $remote_addr" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:9001": 1 } } }' diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index d8e505c94556..51173642d1eb 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -42,7 +42,8 @@ title: limit-count | ------------------- | ------- | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | count | integer | 必须 | | count > 0 | 指定时间窗口内的请求数量阈值 | | time_window | integer | 必须 | | time_window > 0 | 时间窗口的大小(以秒为单位),超过这个时间就会重置 | -| key | string | 可选 | "remote_addr" | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name", "service_id"] | 用来做请求计数的有效值。<br />例如,可以使用主机名(或服务器区域)作为关键字,以便限制每个主机名规定时间内的请求次数。我们也可以使用客户端地址作为关键字,这样我们就可以避免单个客户端规定时间内多次的连接我们的服务。<br />当前接受的 key 有:"remote_addr"(客户端 IP 地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP", "consumer_name"(consumer 的 username), "service_id" 。 | +| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | +| key | string | 可选 | "remote_addr" | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 keys,那么插件会同时受 remote_addr 和 consumer_name 两个 key 的约束。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | | policy | string | 可选 | "local" | ["local", "redis", "redis-cluster"] | 用于检索和增加限制的速率限制策略。可选的值有:`local`(计数器被以内存方式保存在节点本地,默认选项) 和 `redis`(计数器保存在 Redis 服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速);以及`redis-cluster`,跟 redis 功能一样,只是使用 redis 集群方式。 | @@ -56,13 +57,11 @@ title: limit-count | redis_cluster_nodes | array | 当 policy 为 `redis-cluster` 时必填| | | 当使用 `redis-cluster` 限速策略时,该属性是 Redis 集群服务节点的地址列表(至少需要两个地址)。 | | redis_cluster_name | string | 当 policy 为 `redis-cluster` 时必填 | | | 当使用 `redis-cluster` 限速策略时,该属性是 Redis 集群服务节点的名称。 | -**key 是可以被用户自定义的,只需要修改插件的一行代码即可完成。并没有在插件中放开是处于安全的考虑。** - ## 如何使用 ### 开启插件 -下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件: +下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件,并设置 `key_type` 为 `var`: ```shell curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -85,6 +84,30 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 }' ``` +下面是一个示例,在指定的 `route` 上开启了 `limit count` 插件,并设置 `key_type` 为 `var_combination`: + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key_type": "var_combination", + "key": "$consumer_name $remote_addr" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:9001": 1 + } + } +}' +``` + 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-count 插件: ![添加插件](../../../assets/images/plugin/limit-count-1.png) diff --git a/t/control/services.t b/t/control/services.t index 734afcc2b067..c702a7ceb0cd 100644 --- a/t/control/services.t +++ b/t/control/services.t @@ -155,7 +155,7 @@ services: } } --- response_body -{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}} +{"id":"5","plugins":{"limit-count":{"allow_degradation":false,"count":2,"key":"remote_addr","key_type":"var","policy":"local","rejected_code":503,"show_limit_quota_header":true,"time_window":60}},"upstream":{"hash_on":"vars","nodes":[{"host":"127.0.0.1","port":1980,"weight":1}],"pass_host":"pass","scheme":"http","type":"roundrobin"}} diff --git a/t/plugin/limit-count-redis-cluster.t b/t/plugin/limit-count-redis-cluster.t index 0eb2d4818b74..7b2b04f5bb87 100644 --- a/t/plugin/limit-count-redis-cluster.t +++ b/t/plugin/limit-count-redis-cluster.t @@ -238,7 +238,7 @@ unlock with key route#1#redis-cluster "limit-count": { "count": 9999, "time_window": 60, - "key": "http_x_real_ip", + "key": "remote_addr", "policy": "redis-cluster", "redis_cluster_nodes": [ "127.0.0.1:5000", @@ -328,7 +328,7 @@ code: 200 "limit-count": { "count": ]] .. count .. [[, "time_window": 60, - "key": "http_x_real_ip", + "key": "remote_addr", "policy": "redis-cluster", "redis_cluster_nodes": [ "127.0.0.1:5000", @@ -393,7 +393,7 @@ code: 503 "limit-count": { "count": 9999, "time_window": 60, - "key": "http_x_real_ip", + "key": "remote_addr", "policy": "redis-cluster", "allow_degradation": true, "redis_cluster_nodes": [ diff --git a/t/plugin/limit-count.t b/t/plugin/limit-count.t index 298dbcf3c952..b04642a6d755 100644 --- a/t/plugin/limit-count.t +++ b/t/plugin/limit-count.t @@ -56,12 +56,12 @@ done -=== TEST 2: wrong value of key +=== TEST 2: set key empty --- config location /t { content_by_lua_block { local plugin = require("apisix.plugins.limit-count") - local ok, err = plugin.check_schema({count = 2, time_window = 60, rejected_code = 503, key = 'host'}) + local ok, err = plugin.check_schema({count = 2, time_window = 60, rejected_code = 503}) if not ok then ngx.say(err) end @@ -72,7 +72,6 @@ done --- request GET /t --- response_body -property "key" validation failed: matches none of the enum values done --- no_error_log [error] diff --git a/t/plugin/limit-count2.t b/t/plugin/limit-count2.t index e3a2aa034269..016fb2909cea 100644 --- a/t/plugin/limit-count2.t +++ b/t/plugin/limit-count2.t @@ -178,3 +178,239 @@ GET /hello --- error_code: 503 --- response_body {"error_msg":"Requests are too frequent, please try again later."} + + + +=== TEST 6: update route, use new limit configuration +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "http_a", + "key_type": "var" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 7: exceed the burst when key_type is var +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 4 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {a = 1}}) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200,503,503] + + + +=== TEST 8: bypass empty key when key_type is var +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 4 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200,200,200] + + + +=== TEST 9: update route, set key type to var_combination +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "$http_a $http_b", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 10: exceed the burst when key_type is var_combination +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 4 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {a = 1}}) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200,503,503] + + + +=== TEST 11: don`t exceed the burst when key_type is var_combination +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {a = i}}) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[503,200] + + + +=== TEST 12: bypass empty key when key_type is var_combination +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200] +--- error_log +bypass the limit count as the key is empty From eab5606426bb775358469191aa78c2a7d9cebba9 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 5 Nov 2021 09:27:38 +0800 Subject: [PATCH 064/260] fix: add handler for invalid basic auth header values (#5419) --- apisix/plugins/basic-auth.lua | 13 +++++++- t/plugin/basic-auth.t | 57 +++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/apisix/plugins/basic-auth.lua b/apisix/plugins/basic-auth.lua index 1df25daefa86..5e780566310e 100644 --- a/apisix/plugins/basic-auth.lua +++ b/apisix/plugins/basic-auth.lua @@ -80,12 +80,23 @@ local function extract_auth_header(authorization) return nil, err end + if not m then + return nil, "Invalid authorization header format" + end + local decoded = ngx.decode_base64(m[1]) + if not decoded then + return nil, "Failed to decode authentication header: " .. m[1] + end + local res res, err = ngx_re.split(decoded, ":") if err then - return nil, "split authorization err:" .. err + return nil, "Split authorization err:" .. err + end + if #res < 2 then + return nil, "Split authorization err: invalid decoded data: " .. decoded end obj.username = ngx.re.gsub(res[1], "\\s+", "", "jo") diff --git a/t/plugin/basic-auth.t b/t/plugin/basic-auth.t index 79078b1aeb49..a780f3b618f8 100644 --- a/t/plugin/basic-auth.t +++ b/t/plugin/basic-auth.t @@ -163,7 +163,46 @@ GET /hello -=== TEST 6: verify, invalid username +=== TEST 6: verify, invalid basic authorization header +--- request +GET /hello +--- more_headers +Authorization: Bad_header YmFyOmJhcgo= +--- error_code: 401 +--- response_body +{"message":"Invalid authorization header format"} +--- no_error_log +[error] + + + +=== TEST 7: verify, invalid authorization value (bad base64 str) +--- request +GET /hello +--- more_headers +Authorization: Basic aca_a +--- error_code: 401 +--- response_body +{"message":"Failed to decode authentication header: aca_a"} +--- no_error_log +[error] + + + +=== TEST 8: verify, invalid authorization value (no password) +--- request +GET /hello +--- more_headers +Authorization: Basic YmFy +--- error_code: 401 +--- response_body +{"message":"Split authorization err: invalid decoded data: bar"} +--- no_error_log +[error] + + + +=== TEST 9: verify, invalid username --- request GET /hello --- more_headers @@ -176,7 +215,7 @@ Authorization: Basic YmFyOmJhcgo= -=== TEST 7: verify, invalid password +=== TEST 10: verify, invalid password --- request GET /hello --- more_headers @@ -189,7 +228,7 @@ Authorization: Basic Zm9vOmZvbwo= -=== TEST 8: verify +=== TEST 11: verify --- request GET /hello --- more_headers @@ -203,7 +242,7 @@ find consumer foo -=== TEST 9: invalid schema, only one field `username` +=== TEST 12: invalid schema, only one field `username` --- config location /t { content_by_lua_block { @@ -234,7 +273,7 @@ GET /t -=== TEST 10: invalid schema, not field given +=== TEST 13: invalid schema, not field given --- config location /t { content_by_lua_block { @@ -264,7 +303,7 @@ qr/\{"error_msg":"invalid plugins configuration: failed to check the configurati -=== TEST 11: invalid schema, not a table +=== TEST 14: invalid schema, not a table --- config location /t { content_by_lua_block { @@ -293,7 +332,7 @@ GET /t -=== TEST 12: get the default schema +=== TEST 15: get the default schema --- config location /t { content_by_lua_block { @@ -315,7 +354,7 @@ GET /t -=== TEST 13: get the schema by schema_type +=== TEST 16: get the schema by schema_type --- config location /t { content_by_lua_block { @@ -337,7 +376,7 @@ GET /t -=== TEST 14: get the schema by error schema_type +=== TEST 17: get the schema by error schema_type --- config location /t { content_by_lua_block { From 4faa4d228382b9e9765cc353859ec017c41297a4 Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Fri, 5 Nov 2021 09:43:22 +0800 Subject: [PATCH 065/260] docs: fix broken link with wrong path (#5427) --- docs/zh/latest/plugins/gzip.md | 2 +- docs/zh/latest/plugins/real-ip.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/latest/plugins/gzip.md b/docs/zh/latest/plugins/gzip.md index 6343b06ee5bf..7586794f0303 100644 --- a/docs/zh/latest/plugins/gzip.md +++ b/docs/zh/latest/plugins/gzip.md @@ -33,7 +33,7 @@ title: gzip `gzip` 插件能动态设置 `Nginx` 的压缩行为。 -**该插件要求 `APISIX` 运行在 [APISIX-OpenResty](../how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty) 上。** +**该插件要求 `APISIX` 运行在 [APISIX-OpenResty](./how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty) 上。** ## 属性 diff --git a/docs/zh/latest/plugins/real-ip.md b/docs/zh/latest/plugins/real-ip.md index fd3a743034f4..84ffeaf6bf60 100644 --- a/docs/zh/latest/plugins/real-ip.md +++ b/docs/zh/latest/plugins/real-ip.md @@ -35,7 +35,7 @@ title: real-ip 它工作方式和 `Nginx` 里 `ngx_http_realip_module` 模块一样,并且更为灵活。 -**该插件要求 `APISIX` 运行在 [APISIX-OpenResty](../how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty) 上。** +**该插件要求 `APISIX` 运行在 [APISIX-OpenResty](./how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty) 上。** ## 属性 From 043cde3a36680bcf0319862cfc1b0bc28b295355 Mon Sep 17 00:00:00 2001 From: tzssangglass <tzssangglass@gmail.com> Date: Thu, 4 Nov 2021 20:56:19 -0500 Subject: [PATCH 066/260] fix(traffix-split): multiple rules with multiple weighted_upstreams under each rule cause upstream_key duplicate (#5414) --- apisix/plugins/traffic-split.lua | 12 +- t/plugin/traffic-split5.t | 313 +++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 t/plugin/traffic-split5.t diff --git a/apisix/plugins/traffic-split.lua b/apisix/plugins/traffic-split.lua index de6d57048240..028cc97f5538 100644 --- a/apisix/plugins/traffic-split.lua +++ b/apisix/plugins/traffic-split.lua @@ -24,6 +24,7 @@ local pairs = pairs local ipairs = ipairs local type = type local table_insert = table.insert +local tostring = tostring local lrucache = core.lrucache.new({ ttl = 0, count = 512 @@ -187,7 +188,10 @@ local function set_upstream(upstream_info, ctx) local matched_route = ctx.matched_route up_conf.parent = matched_route local upstream_key = up_conf.type .. "#route_" .. - matched_route.value.id .. "_" ..upstream_info.vid + matched_route.value.id .. "_" .. upstream_info.vid + if upstream_info.node_tid then + upstream_key = upstream_key .. "_" .. upstream_info.node_tid + end core.log.info("upstream_key: ", upstream_key) upstream.set(ctx, upstream_key, ctx.conf_version, up_conf) @@ -203,6 +207,12 @@ local function new_rr_obj(weighted_upstreams) elseif upstream_obj.upstream then -- Add a virtual id field to uniquely identify the upstream key. upstream_obj.upstream.vid = i + -- Get the table id of the nodes as part of the upstream_key, + -- avoid upstream_key duplicate because vid is the same in the loop + -- when multiple rules with multiple weighted_upstreams under each rule. + -- see https://github.com/apache/apisix/issues/5276 + local node_tid = tostring(upstream_obj.upstream.nodes):sub(#"table: " + 1) + upstream_obj.upstream.node_tid = node_tid server_list[upstream_obj.upstream] = upstream_obj.weight else -- If the upstream object has only the weight value, it means diff --git a/t/plugin/traffic-split5.t b/t/plugin/traffic-split5.t new file mode 100644 index 000000000000..9e01ac8f69d8 --- /dev/null +++ b/t/plugin/traffic-split5.t @@ -0,0 +1,313 @@ +# +# 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) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]"); + } + + my $http_config = $block->http_config // <<_EOC_; + # fake server, only for test + server { + listen 1970; + location / { + content_by_lua_block { + ngx.say(1970) + } + } + } + + server { + listen 1971; + location / { + content_by_lua_block { + ngx.say(1971) + } + } + } + + server { + listen 1972; + location / { + content_by_lua_block { + ngx.say(1972) + } + } + } + + server { + listen 1973; + location / { + content_by_lua_block { + ngx.say(1973) + } + } + } + + server { + listen 1974; + location / { + content_by_lua_block { + ngx.say(1974) + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: set upstream(multiple rules, multiple nodes under each weighted_upstreams) and add route +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local data = { + uri = "/hello", + plugins = { + ["traffic-split"] = { + rules = { + { + match = { { + vars = { { "arg_id", "==", "1" } } + } }, + weighted_upstreams = { + { + upstream = { + name = "upstream_A", + type = "roundrobin", + nodes = { + ["127.0.0.1:1970"] = 1, + ["127.0.0.1:1971"] = 1 + } + }, + weight = 1 + } + } + }, + { + match = { { + vars = { { "arg_id", "==", "2" } } + } }, + weighted_upstreams = { + { + upstream = { + name = "upstream_B", + type = "roundrobin", + nodes = { + ["127.0.0.1:1972"] = 1, + ["127.0.0.1:1973"] = 1 + } + }, + weight = 1 + } + } + } + } + } + }, + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1974"] = 1 + } + } + } + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + json.encode(data) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 2: hit different weighted_upstreams by rules +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local res, err = httpc:request_uri(uri) + local port = tonumber(res.body) + if port ~= 1974 then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.say("failed while no arg_id") + return + end + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello?id=1" + res, err = httpc:request_uri(uri) + port = tonumber(res.body) + if port ~= 1970 and port ~= 1971 then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.say("failed while arg_id = 1") + return + end + + uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello?id=2" + res, err = httpc:request_uri(uri) + port = tonumber(res.body) + if port ~= 1972 and port ~= 1973 then + ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR + ngx.say("failed while arg_id = 2") + return + end + + ngx.say("passed") + } + } +--- response_body +passed + + + +=== TEST 3: set upstream(multiple rules, multiple nodes with different weight under each weighted_upstreams) and add route +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local data = { + uri = "/hello", + plugins = { + ["traffic-split"] = { + rules = { + { + match = { { + vars = { { "arg_id", "==", "1" } } + } }, + weighted_upstreams = { + { + upstream = { + name = "upstream_A", + type = "roundrobin", + nodes = { + ["127.0.0.1:1970"] = 2, + ["127.0.0.1:1971"] = 1 + } + }, + weight = 1 + } + } + }, + { + match = { { + vars = { { "arg_id", "==", "2" } } + } }, + weighted_upstreams = { + { + upstream = { + name = "upstream_B", + type = "roundrobin", + nodes = { + ["127.0.0.1:1972"] = 2, + ["127.0.0.1:1973"] = 1 + } + }, + weight = 1 + } + } + } + } + } + }, + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1974"] = 1 + } + } + } + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + json.encode(data) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 4: pick different nodes by weight +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local httpc = http.new() + + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello?id=1" + local ports = {} + local res, err + for i = 1, 3 do + res, err = httpc:request_uri(uri) + local port = tonumber(res.body) + ports[i] = port + end + table.sort(ports) + + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello?id=2" + for i = 4, 6 do + res, err = httpc:request_uri(uri) + local port = tonumber(res.body) + ports[i] = port + end + table.sort(ports) + + ngx.say(table.concat(ports, ", ")) + } + } +--- response_body +1970, 1970, 1971, 1972, 1972, 1973 From 61e4d3e1a72cdd8c222f9738511741a14fe66f42 Mon Sep 17 00:00:00 2001 From: Xunzhuo <mixdeers@gmail.com> Date: Fri, 5 Nov 2021 14:27:16 +0800 Subject: [PATCH 067/260] docs(localization): translate client-control from EN into ZH (#5426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 <spacewanderlzx@gmail.com> --- docs/zh/latest/config.json | 3 +- docs/zh/latest/plugins/client-control.md | 101 +++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 docs/zh/latest/plugins/client-control.md diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index d225a6364109..bb8e9876cb81 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -91,7 +91,8 @@ "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", - "plugins/request-id" + "plugins/request-id", + "plugins/client-control" ] }, { diff --git a/docs/zh/latest/plugins/client-control.md b/docs/zh/latest/plugins/client-control.md new file mode 100644 index 000000000000..80654f7c1cd5 --- /dev/null +++ b/docs/zh/latest/plugins/client-control.md @@ -0,0 +1,101 @@ +--- +title: client-control +--- + +<!-- +# +# 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. +# +--> + +## 目录 + +- [**名称**](#名称) +- [**属性**](#属性) +- [**如何启用**](#如何启用) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 名称 + +`client-control` 插件能够动态地控制 Nginx 处理客户端的请求的行为。 + +**这个插件需要 APISIX 在 [APISIX-OpenResty](../how-to-build.md#step-6-build-openresty-for-apache-apisix) 上运行。** + +## 属性 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| --------- | ------------- | ----------- | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| max_body_size | integer | 可选 | | >= 0 | 动态设置 `client_max_body_size` 的大小 | + +## 如何启用 + +以下是一个示例,在指定路由中启用插件: + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "client-control": { + "max_body_size" : 1 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +## 测试插件 + +使用 `curl` 去测试: + +```shell +curl -i http://127.0.0.1:9080/index.html -d '123' + +HTTP/1.1 413 Request Entity Too Large +... +<html> +<head><title>413 Request Entity Too Large + +

413 Request Entity Too Large

+
openresty
+ + +``` + +## 禁用插件 + +当您要禁用 `client-control` 插件时,这很简单,您可以在插件配置中删除相应的 json 配置,无需重新启动服务,它将立即生效: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +现在就已经移除 `client-control` 插件了。其他插件的开启和移除也是同样的方法。 From 308282b9642c5b480aa9326000bb3a643e829f29 Mon Sep 17 00:00:00 2001 From: Xunzhuo Date: Fri, 5 Nov 2021 14:29:10 +0800 Subject: [PATCH 068/260] docs(localization): translate ext-plugin-pre-req from EN into ZH (#5424) --- docs/zh/latest/config.json | 3 +- docs/zh/latest/plugins/ext-plugin-pre-req.md | 96 ++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 docs/zh/latest/plugins/ext-plugin-pre-req.md diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index bb8e9876cb81..da098e3805d2 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -40,7 +40,8 @@ "plugins/echo", "plugins/gzip", "plugins/real-ip", - "plugins/server-info" + "plugins/server-info", + "plugins/ext-plugin-pre-req" ] }, { diff --git a/docs/zh/latest/plugins/ext-plugin-pre-req.md b/docs/zh/latest/plugins/ext-plugin-pre-req.md new file mode 100644 index 000000000000..76e9ae6e09fb --- /dev/null +++ b/docs/zh/latest/plugins/ext-plugin-pre-req.md @@ -0,0 +1,96 @@ +--- +title: ext-plugin-pre-req +--- + + + +## 目录 + +- [**简介**](#简介) +- [**属性**](#属性) +- [**如何启用**](#如何启用) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 简介 + +`ext-plugin-pre-req` 在执行大多数内置 Lua 插件执行之前,在 Plugin Runner 内运行特定 External Plugin。 + +为了理解什么是 Plugin Runner,请参考 [external plugin](../external-plugin.md) 部分。 + +External Plugins 执行的结果会影响当前请求的行为。 + +## 属性 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| --------- | ------------- | ----------- | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| conf | array | 可选 | | [{"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"}] | 在 Plugin Runner 内执行的插件列表的配置 | + +## 如何启用 + +以下是一个示例,在指定路由中启用插件: + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "plugins": { + "ext-plugin-pre-req": { + "conf" : [ + {"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"} + ] + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +## 测试插件 + +使用 `curl` 去测试: + +```shell +curl -i http://127.0.0.1:9080/index.html +``` + +你会看到配置的 Plugin Runner 将会被触发,同时 `ext-plugin-A` 插件将会被执行。 + +## 禁用插件 + +当你想去掉 ext-plugin-pre-req 插件的时候,很简单,在插件的配置中把对应的 json 配置删除即可,无须重启服务,即刻生效: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/index.html", + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +现在就已经移除 `ext-plugin-pre-req` 插件了。其他插件的开启和移除也是同样的方法。 From fc7de8ea2c326dcae7f9c33fddcc5de4c435ff6c Mon Sep 17 00:00:00 2001 From: Xunzhuo Date: Fri, 5 Nov 2021 17:02:43 +0800 Subject: [PATCH 069/260] docs(localization): translate ext-plugin-post-req from EN into ZH (#5423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 琚致远 --- docs/zh/latest/config.json | 1 + docs/zh/latest/plugins/ext-plugin-post-req.md | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docs/zh/latest/plugins/ext-plugin-post-req.md diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index da098e3805d2..986a74ffcbf4 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -41,6 +41,7 @@ "plugins/gzip", "plugins/real-ip", "plugins/server-info", + "plugins/ext-plugin-post-req", "plugins/ext-plugin-pre-req" ] }, diff --git a/docs/zh/latest/plugins/ext-plugin-post-req.md b/docs/zh/latest/plugins/ext-plugin-post-req.md new file mode 100644 index 000000000000..ee6bd62fe156 --- /dev/null +++ b/docs/zh/latest/plugins/ext-plugin-post-req.md @@ -0,0 +1,28 @@ +--- +title: ext-plugin-post-req +--- + + + +`ext-plugin-post-req` 插件的功能与 `ext-plugin-pre-req` 插件类似。 + +唯一不同的是:它在内置 Lua 插件执行之后且在请求到达上游之前工作。 + +参考文档 [ext-plugin-pre-req](./ext-plugin-pre-req.md) 去学习如何配置并使用它。 From b12cdf27aad5f07b69c90f4b60d252489390219c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E8=8C=82=E6=9E=97?= <1090568335@qq.com> Date: Sat, 6 Nov 2021 13:35:58 +0800 Subject: [PATCH 070/260] docs: fix the wrong secret in hmac-auth.md (#5433) Co-authored-by: jon.yu --- docs/en/latest/plugins/hmac-auth.md | 8 ++++---- docs/zh/latest/plugins/hmac-auth.md | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/en/latest/plugins/hmac-auth.md b/docs/en/latest/plugins/hmac-auth.md index 7a23cfff6e82..609eb5854d19 100644 --- a/docs/en/latest/plugins/hmac-auth.md +++ b/docs/en/latest/plugins/hmac-auth.md @@ -345,10 +345,10 @@ Need to pay attention to the handling of newline characters in signature strings Example inputs: -| Variable | Value | -| -------- | ------------------------ | -| secret | this is secret key | -| message | this is signature string | +| Variable | Value | +| -------- | -------------------------- | +| secret | the shared secret key here | +| message | this is signature string | Example outputs: diff --git a/docs/zh/latest/plugins/hmac-auth.md b/docs/zh/latest/plugins/hmac-auth.md index b948fac5b20a..7d6c59689099 100644 --- a/docs/zh/latest/plugins/hmac-auth.md +++ b/docs/zh/latest/plugins/hmac-auth.md @@ -334,10 +334,10 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f 示例入参说明: -| Variable | Value | -| -------- | ------------------------ | -| secret | this is secret key | -| message | this is signature string | +| Variable | Value | +| -------- | -------------------------- | +| secret | the shared secret key here | +| message | this is signature string | 示例出参说明: From 2d8b676cdd6d5535aa5e828950304720d461b1a5 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Sun, 7 Nov 2021 19:26:42 +0800 Subject: [PATCH 071/260] fix(auth-ldap): add handler for invalid basic auth header values (#5432) --- apisix/plugins/ldap-auth.lua | 16 ++++++----- t/plugin/ldap-auth.t | 55 ++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/apisix/plugins/ldap-auth.lua b/apisix/plugins/ldap-auth.lua index 6318523654d6..59b48f04af85 100644 --- a/apisix/plugins/ldap-auth.lua +++ b/apisix/plugins/ldap-auth.lua @@ -94,19 +94,23 @@ local function extract_auth_header(authorization) return nil, err end + if not m then + return nil, "Invalid authorization header format" + end + local decoded = ngx.decode_base64(m[1]) if not decoded then - return nil, "failed to decode authentication header: " .. m[1] + return nil, "Failed to decode authentication header: " .. m[1] end local res res, err = ngx_re.split(decoded, ":") if err then - return nil, "split authorization err:" .. err + return nil, "Split authorization err:" .. err end if #res < 2 then - return nil, "split authorization err: invalid decoded data: " .. decoded + return nil, "Split authorization err: invalid decoded data: " .. decoded end obj.username = ngx.re.gsub(res[1], "\\s+", "", "jo") @@ -131,10 +135,8 @@ function _M.rewrite(conf, ctx) end -- 2. try authenticate the user against the ldap server - local uid = "cn" - if conf.uid then - uid = conf.uid - end + local uid = conf.uid or "cn" + local userdn = uid .. "=" .. user.username .. "," .. conf.base_dn local ld = lualdap.open_simple (conf.ldap_uri, userdn, user.password, conf.use_tls) if not ld then diff --git a/t/plugin/ldap-auth.t b/t/plugin/ldap-auth.t index 5674cb8b3413..148032319914 100644 --- a/t/plugin/ldap-auth.t +++ b/t/plugin/ldap-auth.t @@ -160,7 +160,46 @@ GET /hello -=== TEST 6: verify, invalid password +=== TEST 6: verify, invalid basic authorization header +--- request +GET /hello +--- more_headers +Authorization: Bad_header Zm9vOmZvbwo= +--- error_code: 401 +--- response_body +{"message":"Invalid authorization header format"} +--- no_error_log +[error] + + + +=== TEST 7: verify, invalid authorization value (bad base64 str) +--- request +GET /hello +--- more_headers +Authorization: Basic aca_a +--- error_code: 401 +--- response_body +{"message":"Failed to decode authentication header: aca_a"} +--- no_error_log +[error] + + + +=== TEST 8: verify, invalid authorization value (no password) +--- request +GET /hello +--- more_headers +Authorization: Basic Zm9v +--- error_code: 401 +--- response_body +{"message":"Split authorization err: invalid decoded data: foo"} +--- no_error_log +[error] + + + +=== TEST 9: verify, invalid password --- request GET /hello --- more_headers @@ -171,7 +210,7 @@ Authorization: Basic Zm9vOmZvbwo= -=== TEST 7: verify +=== TEST 10: verify --- request GET /hello --- more_headers @@ -183,7 +222,7 @@ find consumer user01 -=== TEST 8: enable basic auth plugin using admin api +=== TEST 11: enable basic auth plugin using admin api --- config location /t { content_by_lua_block { @@ -219,7 +258,7 @@ passed -=== TEST 9: verify +=== TEST 12: verify --- request GET /hello --- more_headers @@ -231,7 +270,7 @@ find consumer user01 -=== TEST 10: invalid schema +=== TEST 13: invalid schema --- config location /t { content_by_lua_block { @@ -259,7 +298,7 @@ find consumer user01 -=== TEST 11: get the default schema +=== TEST 14: get the default schema --- config location /t { content_by_lua_block { @@ -277,7 +316,7 @@ find consumer user01 -=== TEST 12: get the schema by schema_type +=== TEST 15: get the schema by schema_type --- config location /t { content_by_lua_block { @@ -295,7 +334,7 @@ find consumer user01 -=== TEST 13: get the schema by error schema_type +=== TEST 16: get the schema by error schema_type --- config location /t { content_by_lua_block { From 0e973fbb508b180c0be21bde41cd1363c5fa61f9 Mon Sep 17 00:00:00 2001 From: Xunzhuo Date: Sun, 7 Nov 2021 19:29:27 +0800 Subject: [PATCH 072/260] feat(limit-* plugin): fallback to remote_addr when key is missing (#5422) --- apisix/plugins/limit-conn/init.lua | 7 +- apisix/plugins/limit-count.lua | 8 +- apisix/plugins/limit-req.lua | 7 +- docs/en/latest/plugins/limit-conn.md | 33 ++++++- docs/en/latest/plugins/limit-count.md | 2 +- docs/en/latest/plugins/limit-req.md | 32 ++++++- docs/zh/latest/plugins/limit-conn.md | 32 ++++++- docs/zh/latest/plugins/limit-count.md | 3 +- docs/zh/latest/plugins/limit-req.md | 30 ++++++- t/plugin/limit-conn2.t | 119 +++++++++++++++++++++----- t/plugin/limit-count2.t | 86 +++++++++++++++++-- t/plugin/limit-req.t | 2 +- t/plugin/limit-req2.t | 81 +++++++++++++++++- t/stream-plugin/limit-conn.t | 6 +- 14 files changed, 389 insertions(+), 59 deletions(-) diff --git a/apisix/plugins/limit-conn/init.lua b/apisix/plugins/limit-conn/init.lua index adc1e6879c21..8b404a528bf5 100644 --- a/apisix/plugins/limit-conn/init.lua +++ b/apisix/plugins/limit-conn/init.lua @@ -64,10 +64,9 @@ function _M.increase(conf, ctx) end if key == nil then - core.log.info("bypass the limit conn as the key is empty") - -- Bypass the limit conn when the key is empty. - -- This behavior is the same as Nginx - return + core.log.info("The value of the configured key is empty, use client IP instead") + -- When the value of key is empty, use client IP instead + key = ctx.var["remote_addr"] end key = key .. ctx.conf_type .. ctx.conf_version diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua index 1a52ef4e73b8..cbce3a798c6c 100644 --- a/apisix/plugins/limit-count.lua +++ b/apisix/plugins/limit-count.lua @@ -187,11 +187,11 @@ function _M.access(conf, ctx) end if key == nil then - core.log.info("bypass the limit count as the key is empty") - -- Bypass the limit count when the key is empty. - -- This behavior is the same as Nginx - return + core.log.info("The value of the configured key is empty, use client IP instead") + -- When the value of key is empty, use client IP instead + key = ctx.var["remote_addr"] end + key = key .. ctx.conf_type .. ctx.conf_version core.log.info("limit key: ", key) diff --git a/apisix/plugins/limit-req.lua b/apisix/plugins/limit-req.lua index 6768dc1f35a4..824a47d4ed12 100644 --- a/apisix/plugins/limit-req.lua +++ b/apisix/plugins/limit-req.lua @@ -102,10 +102,9 @@ function _M.access(conf, ctx) end if key == nil then - core.log.info("bypass the limit req as the key is empty") - -- Bypass the limit req when the key is empty. - -- This behavior is the same as Nginx - return + core.log.info("The value of the configured key is empty, use client IP instead") + -- When the value of key is empty, use client IP instead + key = ctx.var["remote_addr"] end key = key .. ctx.conf_type .. ctx.conf_version diff --git a/docs/en/latest/plugins/limit-conn.md b/docs/en/latest/plugins/limit-conn.md index 479003916565..37f15c4609c8 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -42,14 +42,14 @@ Limiting request concurrency plugin. | default_conn_delay | number | required | | default_conn_delay > 0 | the latency seconds of request when concurrent requests exceeding `conn` but below (`conn` + `burst`). | | only_use_default_delay | boolean | optional | false | [true,false] | enable the strict mode of the latency seconds. If you set this option to `true`, it will run strictly according to the latency seconds you set without additional calculation logic. | | key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | -| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr $consumer_name". | +| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr $consumer_name". If the value of the key is empty, `remote_addr` will be set as the default key.| | rejected_code | string | optional | 503 | [200,...,599] | the HTTP status code returned when the request exceeds `conn` + `burst` will be rejected. | | rejected_msg | string | optional | | non-empty | the response body returned when the request exceeds `conn` + `burst` will be rejected. | | allow_degradation | boolean | optional | false | | Whether to enable plugin degradation when the limit-conn function is temporarily unavailable. Allow requests to continue when the value is set to true, default false. | ## How To Enable -Here's an example, enable the limit-conn plugin on the specified route: +Here's an example, enable the limit-conn plugin on the specified route when setting `key_type` to `var` : ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -62,7 +62,34 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "burst": 0, "default_conn_delay": 0.1, "rejected_code": 503, - "key": "remote_addr" + "key_type": "var", + "key": "http_a" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +Here's an example, enable the limit-conn plugin on the specified route when setting `key_type` to `var_combination` : + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/index.html", + "plugins": { + "limit-conn": { + "conn": 1, + "burst": 0, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key_type": "var_combination", + "key": "$consumer_name $remote_addr" } }, "upstream": { diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 2c267603f0bf..ebd379545160 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -40,7 +40,7 @@ Limit request rate by a fixed number of requests in a given time window. | count | integer | required | | count > 0 | the specified number of requests threshold. | | time_window | integer | required | | time_window > 0 | the time window in seconds before the request count is reset. | | key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | -| key | string | optional | "remote_addr" | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable. If the `key_type` is "var_combination", the key will be a combination of variables. For example, if we use "$remote_addr $consumer_name" as keys, plugin will be restricted by two keys which are "remote_addr" and "consumer_name". | +| key | string | optional | "remote_addr" | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable. If the `key_type` is "var_combination", the key will be a combination of variables. For example, if we use "$remote_addr $consumer_name" as keys, plugin will be restricted by two keys which are "remote_addr" and "consumer_name". If the value of the key is empty, `remote_addr` will be set as the default key.| | rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected, default 503. | | rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. | | policy | string | optional | "local" | ["local", "redis", "redis-cluster"] | The rate-limiting policies to use for retrieving and incrementing the limits. Available values are `local`(the counters will be stored locally in-memory on the node), `redis`(counters are stored on a Redis server and will be shared across the nodes, usually use it to do the global speed limit), and `redis-cluster` which works the same as `redis` but with redis cluster. | diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index 0c16c766ecc0..f63eb93d31a5 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -41,7 +41,7 @@ limit request rate using the "leaky bucket" method. | rate | integer | required | | rate > 0 | the specified request rate (number per second) threshold. Requests exceeding this rate (and below `burst`) will get delayed to conform to the rate. | | burst | integer | required | | burst >= 0 | the number of excessive requests per second allowed to be delayed. Requests exceeding this hard limit will get rejected immediately. | | key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | -| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr $consumer_name". | +| key | string | required | | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable, like "remote_addr" or "consumer_name". If the `key_type` is "var_combination", the key will be a combination of variables, like "$remote_addr $consumer_name". If the value of the key is empty, `remote_addr` will be set as the default key.| | rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected. | | rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. | | nodelay | boolean | optional | false | | If nodelay flag is true, bursted requests will not get delayed | @@ -51,7 +51,7 @@ limit request rate using the "leaky bucket" method. ### How to enable on the `route` or `service` -Take `route` as an example (the use of `service` is the same method), enable the `limit-req` plugin on the specified route. +Take `route` as an example (the use of `service` is the same method), enable the `limit-req` plugin on the specified route when setting `key_type` to `var` . ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -63,13 +63,39 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "rate": 1, "burst": 2, "rejected_code": 503, + "key_type": "var", "key": "remote_addr" } }, "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:9001": 1 + } + } +}' +``` + +Take `route` as an example (the use of `service` is the same method), enable the `limit-req` plugin on the specified route when setting `key_type` to `var_combination` . + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/index.html", + "plugins": { + "limit-req": { + "rate": 1, + "burst": 2, + "rejected_code": 503, + "key_type": "var_combination", + "key": "$consumer_name $remote_addr" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:9001": 1 } } }' diff --git a/docs/zh/latest/plugins/limit-conn.md b/docs/zh/latest/plugins/limit-conn.md index ca3a381fcc3b..1fdaa628dc5a 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -32,14 +32,14 @@ title: limit-conn | default_conn_delay | number | required | | default_conn_delay > 0 | 默认的典型连接(或请求)的处理延迟时间。 | | only_use_default_delay | boolean | optional | false | [true,false] | 延迟时间的严格模式。 如果设置为`true`的话,将会严格按照设置的时间来进行延迟 | | key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | -| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。 | +| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。如果 key 的值为空,$remote_addr 会被作为默认 key。 | | rejected_code | string | optional | 503 | [200,...,599] | 当请求超过 `conn` + `burst` 这个阈值时,返回的 HTTP 状态码 | | rejected_msg | string | 可选 | | 非空 | 当请求超过 `conn` + `burst` 这个阈值时,返回的响应体。 | | allow_degradation | boolean | 可选 | false | | 当插件功能临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| #### 如何启用 -下面是一个示例,在指定的 route 上开启了 limit-conn 插件: +下面是一个示例,在指定的 route 上开启了 limit-conn 插件,并设置 `key_type` 为 `var`: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -53,6 +53,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "burst": 0, "default_conn_delay": 0.1, "rejected_code": 503, + "key_type": "var", "key": "remote_addr" } }, @@ -65,6 +66,33 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +下面是一个示例,在指定的 route 上开启了 limit-conn 插件,并设置 `key_type` 为 `var_combination`: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/index.html", + "id": 1, + "plugins": { + "limit-conn": { + "conn": 1, + "burst": 0, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key_type": "var_combination", + "key": "$consumer_name $remote_addr" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-conn 插件: ![enable limit-conn plugin](../../../assets/images/plugin/limit-conn-1.png) diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index 51173642d1eb..7c9f3098961d 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -43,7 +43,7 @@ title: limit-count | count | integer | 必须 | | count > 0 | 指定时间窗口内的请求数量阈值 | | time_window | integer | 必须 | | time_window > 0 | 时间窗口的大小(以秒为单位),超过这个时间就会重置 | | key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | -| key | string | 可选 | "remote_addr" | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 keys,那么插件会同时受 remote_addr 和 consumer_name 两个 key 的约束。 | +| key | string | 可选 | "remote_addr" | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 keys,那么插件会同时受 remote_addr 和 consumer_name 两个 key 的约束。如果 key 的值为空,$remote_addr 会被作为默认 key。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | | policy | string | 可选 | "local" | ["local", "redis", "redis-cluster"] | 用于检索和增加限制的速率限制策略。可选的值有:`local`(计数器被以内存方式保存在节点本地,默认选项) 和 `redis`(计数器保存在 Redis 服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速);以及`redis-cluster`,跟 redis 功能一样,只是使用 redis 集群方式。 | @@ -72,6 +72,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "count": 2, "time_window": 60, "rejected_code": 503, + "key_type": "var", "key": "remote_addr" } }, diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index d0e8dd9c9e34..a7d88ca7f827 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -41,7 +41,7 @@ title: limit-req | rate | integer | 必须 | | rate > 0 | 指定的请求速率(以秒为单位),请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求会被加上延时。 | | burst | integer | 必须 | | burst >= 0 | 请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 | | key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | -| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。 | +| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。如果 key 的值为空,$remote_addr 会被作为默认 key。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码。 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | | nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求不会加上延迟, 如果是 false,则会加上延迟。 | @@ -51,7 +51,7 @@ title: limit-req ### 如何在`route`或`service`上使用 -这里以`route`为例(`service`的使用是同样的方法),在指定的 `route` 上启用 `limit-req` 插件。 +这里以`route`为例(`service`的使用是同样的方法),在指定的 `route` 上启用 `limit-req` 插件,并设置 `key_type` 为 `var`。 ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -63,6 +63,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "rate": 1, "burst": 2, "rejected_code": 503, + "key_type": "var", "key": "remote_addr" } }, @@ -75,6 +76,31 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +这里以`route`为例(`service`的使用是同样的方法),在指定的 `route` 上启用 `limit-req` 插件,并设置 `key_type` 为 `var_combination`。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/index.html", + "plugins": { + "limit-req": { + "rate": 1, + "burst": 2, + "rejected_code": 503, + "key_type": "var_combination", + "key": "$consumer_name $remote_addr" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-req 插件: ![添加插件](../../../assets/images/plugin/limit-req-1.png) diff --git a/t/plugin/limit-conn2.t b/t/plugin/limit-conn2.t index 187e3aa74b5a..75375aaed8bf 100644 --- a/t/plugin/limit-conn2.t +++ b/t/plugin/limit-conn2.t @@ -34,6 +34,36 @@ no_root_location(); add_block_preprocessor(sub { my ($block) = @_; + my $port = $ENV{TEST_NGINX_SERVER_PORT}; + + my $config = $block->config // <<_EOC_; + location /access_root_dir { + content_by_lua_block { + local httpc = require "resty.http" + local hc = httpc:new() + + local res, err = hc:request_uri('http://127.0.0.1:$port/limit_conn') + if res then + ngx.exit(res.status) + end + } + } + + location /test_concurrency { + content_by_lua_block { + local reqs = {} + for i = 1, 5 do + reqs[i] = { "/access_root_dir" } + end + local resps = { ngx.location.capture_multi(reqs) } + for i, resp in ipairs(resps) do + ngx.say(resp.status) + end + } + } +_EOC_ + + $block->set_value("config", $config); if (!$block->request) { $block->set_value("request", "GET /t"); @@ -311,7 +341,7 @@ request latency is nil -=== TEST 9: set key type to var_combination +=== TEST 9: update plugin to set key_type to var_combination --- config location /t { content_by_lua_block { @@ -321,8 +351,8 @@ request latency is nil [[{ "plugins": { "limit-conn": { - "conn": 2, - "burst": 1, + "conn": 1, + "burst": 0, "default_conn_delay": 0.1, "rejected_code": 503, "key": "$http_a $http_b", @@ -342,8 +372,8 @@ request latency is nil "value": { "plugins": { "limit-conn": { - "conn": 2, - "burst": 1, + "conn": 1, + "burst": 0, "default_conn_delay": 0.1, "rejected_code": 503, "key": "$http_a $http_b", @@ -410,32 +440,77 @@ GET /t -=== TEST 11: bypass empty key +=== TEST 11: request when key is missing +--- request +GET /test_concurrency +--- timeout: 10s +--- response_body +200 +503 +503 +503 +503 +--- no_error_log +[error] +--- error_log +The value of the configured key is empty, use client IP instead + + + +=== TEST 12: update plugin to set invalid key --- config location /t { content_by_lua_block { - local json = require "t.toolkit.json" - local http = require "resty.http" - local uri = "http://127.0.0.1:" .. ngx.var.server_port - .. "/limit_conn" - local ress = {} - for i = 1, 2 do - local httpc = http.new() - local res, err = httpc:request_uri(uri) - if not res then - ngx.say(err) - return - end - table.insert(ress, res.status) + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-conn": { + "conn": 1, + "burst": 0, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key": "abcdefgh", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/limit_conn" + }]] + ) + + if code >= 300 then + ngx.status = code end - ngx.say(json.encode(ress)) + ngx.say(body) } } --- request GET /t +--- response_body +passed --- no_error_log [error] + + + +=== TEST 13: request when key is invalid +--- request +GET /test_concurrency +--- timeout: 10s --- response_body -[200,200] +200 +503 +503 +503 +503 +--- no_error_log +[error] --- error_log -bypass the limit conn as the key is empty +The value of the configured key is empty, use client IP instead diff --git a/t/plugin/limit-count2.t b/t/plugin/limit-count2.t index 016fb2909cea..ea4a675c2db9 100644 --- a/t/plugin/limit-count2.t +++ b/t/plugin/limit-count2.t @@ -279,11 +279,11 @@ GET /t --- no_error_log [error] --- response_body -[200,200,200,200] +[200,200,503,503] -=== TEST 9: update route, set key type to var_combination +=== TEST 9: update plugin to set key_type to var_combination --- config location /t { content_by_lua_block { @@ -385,7 +385,7 @@ GET /t -=== TEST 12: bypass empty key when key_type is var_combination +=== TEST 12: request when key is missing --- config location /t { content_by_lua_block { @@ -394,7 +394,81 @@ GET /t local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" local ress = {} - for i = 1, 2 do + for i = 1, 4 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,200,503,503] +--- error_log +The value of the configured key is empty, use client IP instead + + + +=== TEST 13: update plugin to set invalid key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "abcdefgh", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 14: request when key is invalid +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + for i = 1, 4 do local httpc = http.new() local res, err = httpc:request_uri(uri) if not res then @@ -411,6 +485,6 @@ GET /t --- no_error_log [error] --- response_body -[200,200] +[200,200,503,503] --- error_log -bypass the limit count as the key is empty +The value of the configured key is empty, use client IP instead diff --git a/t/plugin/limit-req.t b/t/plugin/limit-req.t index d3d03e30fa6f..d0634cb87eb6 100644 --- a/t/plugin/limit-req.t +++ b/t/plugin/limit-req.t @@ -696,7 +696,7 @@ GET /hello --- response_body hello world --- error_log -bypass the limit req as the key is empty +The value of the configured key is empty, use client IP instead diff --git a/t/plugin/limit-req2.t b/t/plugin/limit-req2.t index dc3e4ce9dcea..4e3793322082 100644 --- a/t/plugin/limit-req2.t +++ b/t/plugin/limit-req2.t @@ -226,7 +226,7 @@ GET /t -=== TEST 8: bypass empty key +=== TEST 8: request when key is missing --- config location /t { content_by_lua_block { @@ -253,6 +253,81 @@ GET /t --- no_error_log [error] --- response_body -[200,200] +[200,503] +--- error_log +The value of the configured key is empty, use client IP instead + + + +=== TEST 9: update plugin to set invalid key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-req": { + "rate": 0.1, + "burst": 0.1, + "rejected_code": 503, + "key": "abcdefgh", + "key_type": "var_combination" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 10: request when key is invalid +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + + local ress = {} + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- request +GET /t +--- no_error_log +[error] +--- response_body +[200,503] --- error_log -bypass the limit req as the key is empty +The value of the configured key is empty, use client IP instead diff --git a/t/stream-plugin/limit-conn.t b/t/stream-plugin/limit-conn.t index c166aacb674f..c6c7c89c0c0d 100644 --- a/t/stream-plugin/limit-conn.t +++ b/t/stream-plugin/limit-conn.t @@ -329,8 +329,8 @@ GET /test_concurrency 200 200 200 -200 -200 +503 +503 --- error_log -bypass the limit conn as the key is empty +The value of the configured key is empty, use client IP instead --- stream_enable From 9caf9f18652b262b5b92fe671391cfd00dac3a24 Mon Sep 17 00:00:00 2001 From: brianzhangrong <39478871+brianzhangrong@users.noreply.github.com> Date: Mon, 8 Nov 2021 10:21:08 +0800 Subject: [PATCH 073/260] feat(nacos): be compatible with password contains @ (#5420) --- apisix/discovery/nacos.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/discovery/nacos.lua b/apisix/discovery/nacos.lua index 2eee156ffae8..663309b7bb95 100644 --- a/apisix/discovery/nacos.lua +++ b/apisix/discovery/nacos.lua @@ -190,7 +190,7 @@ local function get_base_uri() local host = local_conf.discovery.nacos.host -- TODO Add health check to get healthy nodes. local url = host[math_random(#host)] - local auth_idx = str_find(url, '@') + local auth_idx = core.string.rfind_char(url, '@') local username, password if auth_idx then local protocol_idx = str_find(url, '://') From cd29ba3be7e4f57e9f348838fef8242c662772d5 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Sun, 7 Nov 2021 20:52:09 -0600 Subject: [PATCH 074/260] fix(admin): modify boolean parameters with PATCH (#5434) --- apisix/admin/init.lua | 2 +- apisix/admin/routes.lua | 2 +- t/admin/routes3.t | 82 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/apisix/admin/init.lua b/apisix/admin/init.lua index f169d5aec6c9..9fec46403068 100644 --- a/apisix/admin/init.lua +++ b/apisix/admin/init.lua @@ -157,7 +157,7 @@ local function run() if req_body then local data, err = core.json.decode(req_body) - if not data then + if err then core.log.error("invalid request body: ", req_body, " err: ", err) core.response.exit(400, {error_msg = "invalid request body: " .. err, req_body = req_body}) diff --git a/apisix/admin/routes.lua b/apisix/admin/routes.lua index bed0524e8384..c3705d4ad476 100644 --- a/apisix/admin/routes.lua +++ b/apisix/admin/routes.lua @@ -247,7 +247,7 @@ function _M.patch(id, conf, sub_path, args) return 400, {error_msg = "missing route id"} end - if not conf then + if conf == nil then return 400, {error_msg = "missing new configuration"} end diff --git a/t/admin/routes3.t b/t/admin/routes3.t index 6f0b13fc417e..02fbc8711ba0 100644 --- a/t/admin/routes3.t +++ b/t/admin/routes3.t @@ -700,3 +700,85 @@ passed } --- response_body passed + + + +=== TEST 20: set route(id: 1, parameters with boolean values) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/index.html", + "enable_websocket": true, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:8080":1 + } + } + }]]) + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 21: patch route(modify the boolean value of parameters to false) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1/enable_websocket', + ngx.HTTP_PATCH, + 'false', + [[{ + "node": { + "value": { + "enable_websocket": false + }, + "key": "/apisix/routes/1" + }, + "action": "compareAndSwap" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 22: patch route(modify the boolean value of parameters to true) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1/enable_websocket', + ngx.HTTP_PATCH, + 'true', + [[{ + "node": { + "value": { + "enable_websocket": true + }, + "key": "/apisix/routes/1" + }, + "action": "compareAndSwap" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed From c2f5eba5cf86bb8f840279f642e63c4d3ba740fa Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Mon, 8 Nov 2021 19:02:56 -0600 Subject: [PATCH 075/260] feat: support advanced matching based on post form (#5409) --- apisix/core/ctx.lua | 14 +++++ apisix/core/request.lua | 19 +++++++ docs/en/latest/router-radixtree.md | 25 +++++++++ docs/zh/latest/router-radixtree.md | 25 +++++++++ t/core/ctx2.t | 76 +++++++++++++++++++++++++++ t/core/request.t | 30 +++++++++++ t/node/vars.t | 84 ++++++++++++++++++++++++++++++ 7 files changed, 273 insertions(+) diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua index dc4c4460e7cb..fb77a378dd9d 100644 --- a/apisix/core/ctx.lua +++ b/apisix/core/ctx.lua @@ -175,6 +175,20 @@ do end end + elseif core_str.has_prefix(key, "post_arg_") then + -- only match default post form + if request.header(nil, "Content-Type") == "application/x-www-form-urlencoded" then + local arg_key = sub_str(key, 10) + local args = request.get_post_args()[arg_key] + if args then + if type(args) == "table" then + val = args[1] + else + val = args + end + end + end + elseif core_str.has_prefix(key, "http_") then key = key:lower() key = re_gsub(key, "-", "_", "jo") diff --git a/apisix/core/request.lua b/apisix/core/request.lua index e43a647a16fb..d5482e6db72c 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -29,6 +29,7 @@ local io_open = io.open local req_read_body = ngx.req.read_body local req_get_body_data = ngx.req.get_body_data local req_get_body_file = ngx.req.get_body_file +local req_get_post_args = ngx.req.get_post_args local req_get_uri_args = ngx.req.get_uri_args local req_set_uri_args = ngx.req.set_uri_args @@ -150,6 +151,24 @@ function _M.set_uri_args(ctx, args) end +function _M.get_post_args(ctx) + if not ctx then + ctx = ngx.ctx.api_ctx + end + + if not ctx.req_post_args then + req_read_body() + + -- use 0 to avoid truncated result and keep the behavior as the + -- same as other platforms + local args = req_get_post_args(0) + ctx.req_post_args = args + end + + return ctx.req_post_args +end + + local function get_file(file_name) local f, err = io_open(file_name, 'r') if not f then diff --git a/docs/en/latest/router-radixtree.md b/docs/en/latest/router-radixtree.md index a65e90cac180..639fcc985ef2 100644 --- a/docs/en/latest/router-radixtree.md +++ b/docs/en/latest/router-radixtree.md @@ -218,6 +218,31 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f This route will require the request header `host` equal `iresty.com`, request cookie key `_device_id` equal `a66f0cdc4ba2df8c096f74c9110163a9` etc. +### How to filter route by POST form attributes + +APISIX supports filtering route by POST form attributes with `Content-Type` = `application/x-www-form-urlencoded`. + +We can define the following route: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' +{ + "methods": ["POST"], + "uri": "/_post", + "vars": [ + ["post_arg_name", "==", "json"] + ], + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +The route will be matched when the POST form contains `name=json`. + ### How to filter route by GraphQL attributes APISIX supports filtering route by some attributes of GraphQL. Currently we support: diff --git a/docs/zh/latest/router-radixtree.md b/docs/zh/latest/router-radixtree.md index 7da9dab39333..f502ecbddc30 100644 --- a/docs/zh/latest/router-radixtree.md +++ b/docs/zh/latest/router-radixtree.md @@ -220,6 +220,31 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f 这个路由需要请求头 `host` 等于 `iresty.com` , 请求 cookie `_device_id` 等于 `a66f0cdc4ba2df8c096f74c9110163a9` 等。 +### 如何通过 POST 表单属性过滤路由 + +APISIX 支持通过 POST 表单属性过滤路由,其中需要您使用 `Content-Type` = `application/x-www-form-urlencoded` 的 POST 请求。 + +我们可以定义这样的路由: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' +{ + "methods": ["POST"], + "uri": "/_post", + "vars": [ + ["post_arg_name", "==", "json"] + ], + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 1 + } + } +}' +``` + +当 POST 表单中包含 `name=json` 的属性时,将匹配到路由。 + ### 如何通过 GraphQL 属性过滤路由 APISIX 支持通过 GraphQL 的一些属性过滤路由。 目前我们支持: diff --git a/t/core/ctx2.t b/t/core/ctx2.t index 33c05d7a51b2..7de032f9ed7f 100644 --- a/t/core/ctx2.t +++ b/t/core/ctx2.t @@ -240,3 +240,79 @@ GET /hello --- error_code: 404 --- response_body {"error_msg":"404 Route Not Found"} + + + +=== TEST 11: parsed post args is cached under ctx +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions" : ["return function(conf, ctx) ngx.log(ngx.WARN, 'find ctx.req_post_args.test: ', ctx.req_post_args.test ~= nil) end"] + } + }, + "uri": "/hello", + "vars": [["post_arg_test", "==", "test"]] + }]=] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 12: hit +--- request +POST /hello +test=test +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- response_body +hello world +--- error_log +find ctx.req_post_args.test: true + + + +=== TEST 13: missed (post_arg_test is missing) +--- request +POST /hello +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 404 +--- response_body +{"error_msg":"404 Route Not Found"} + + + +=== TEST 14: missed (post_arg_test is mismatch) +--- request +POST /hello +test=tesy +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 404 +--- response_body +{"error_msg":"404 Route Not Found"} diff --git a/t/core/request.t b/t/core/request.t index fb4c4c6201ee..e9dca7b8f1b9 100644 --- a/t/core/request.t +++ b/t/core/request.t @@ -378,3 +378,33 @@ nil t --- no_error_log [error] + + + +=== TEST 11: get_post_args +--- config + location = /hello { + content_by_lua_block { + local core = require("apisix.core") + local ngx_ctx = ngx.ctx + local api_ctx = ngx_ctx.api_ctx + if api_ctx == nil then + api_ctx = core.tablepool.fetch("api_ctx", 0, 32) + ngx_ctx.api_ctx = api_ctx + end + + core.ctx.set_vars_meta(api_ctx) + + local args = core.request.get_post_args(ngx.ctx.api_ctx) + ngx.say(args["c"]) + ngx.say(args["v"]) + } + } +--- request +POST /hello +c=z_z&v=x%20x +--- response_body +z_z +x x +--- no_error_log +[error] diff --git a/t/node/vars.t b/t/node/vars.t index 1467a0cc2b1b..589fc19f9306 100644 --- a/t/node/vars.t +++ b/t/node/vars.t @@ -299,3 +299,87 @@ GET /hello?k=uri_arg hello world --- no_error_log [error] + + + +=== TEST 17: set route(only post arg) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "uri": "/hello", + "vars": [["post_arg_k", "==", "post_form"]], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]=] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 18: not_found (GET request) +--- request +GET /hello +--- error_code: 404 +--- response_body +{"error_msg":"404 Route Not Found"} +--- no_error_log +[error] + + + +=== TEST 19: not_found (wrong request body) +--- request +POST /hello +123 +--- error_code: 404 +--- response_body +{"error_msg":"404 Route Not Found"} +--- no_error_log +[error] + + + +=== TEST 20: not_found (wrong content type) +--- request +POST /hello +k=post_form +--- more_headers +Content-Type: multipart/form-data +--- error_code: 404 +--- response_body +{"error_msg":"404 Route Not Found"} +--- no_error_log +[error] + + + +=== TEST 21: hit routes +--- request +POST /hello +k=post_form +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- response_body +hello world +--- no_error_log +[error] From cc6caa974ca30873a8f6193407d7b65f32a36390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 9 Nov 2021 09:16:31 +0800 Subject: [PATCH 076/260] change: log insensitive consumer info only (#5445) --- apisix/utils/log-util.lua | 9 +++- t/plugin/http-logger-log-format.t | 69 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/apisix/utils/log-util.lua b/apisix/utils/log-util.lua index 361d9b264c21..cf3fc22f1c3b 100644 --- a/apisix/utils/log-util.lua +++ b/apisix/utils/log-util.lua @@ -84,6 +84,13 @@ local function get_full_log(ngx, conf) service_id = var.host end + local consumer + if ctx.consumer then + consumer = { + username = ctx.consumer.username + } + end + local log = { request = { url = url, @@ -105,7 +112,7 @@ local function get_full_log(ngx, conf) upstream = var.upstream_addr, service_id = service_id, route_id = route_id, - consumer = ctx.consumer, + consumer = consumer, client_ip = core.request.get_remote_client_ip(ngx.ctx.api_ctx), start_time = ngx.req.start_time() * 1000, latency = (ngx.now() - ngx.req.start_time()) * 1000 diff --git a/t/plugin/http-logger-log-format.t b/t/plugin/http-logger-log-format.t index 703418b464f4..07978f5c197d 100644 --- a/t/plugin/http-logger-log-format.t +++ b/t/plugin/http-logger-log-format.t @@ -364,3 +364,72 @@ GET /t passed --- no_error_log [error] + + + +=== TEST 12: check default log format +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "auth-one" + } + } + }]] + ) + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "http-logger": { + "uri": "http://127.0.0.1:1982/log", + "batch_max_size": 1, + "max_retry_count": 1, + "retry_delay": 2, + "buffer_duration": 2, + "inactive_timeout": 2 + }, + "key-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 13: hit +--- request +GET /hello +--- more_headers +apikey: auth-one +--- grep_error_log eval +qr/request log: \{.+\}/ +--- grep_error_log_out eval +qr/\Q{"client_ip":"127.0.0.1","consumer":{"username":"jack"},"latency":\E[^,]+\Q,"request":{"headers":{"apikey":"auth-one","connection":"close","host":"localhost"},"method":"GET","querystring":{},"size":\E\d+\Q,"uri":"\/hello","url":"http:\/\/localhost:1984\/hello"},"response":{"headers":{"connection":"close","content-length":"\E\d+\Q","content-type":"text\/plain","server":"\E[^"]+\Q"},"size":\E\d+\Q,"status":200},"route_id":"1","server":{"hostname":"\E[^"]+\Q","version":"\E[^"]+\Q"},"service_id":"","start_time":\E\d+\Q,"upstream":"127.0.0.1:1982"}\E/ +--- wait: 0.5 From da691b92b819d40ffa1df6b1b76a0d4012606fc8 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Tue, 9 Nov 2021 07:05:30 +0530 Subject: [PATCH 077/260] feat(plugins): Datadog for metrics collection (#5372) --- apisix/plugins/datadog.lua | 277 +++++++++++++++++++++ conf/config-default.yaml | 1 + docs/en/latest/config.json | 3 +- docs/en/latest/plugins/datadog.md | 131 ++++++++++ t/admin/plugins.t | 2 +- t/lib/mock_dogstatsd.lua | 45 ++++ t/plugin/datadog.t | 397 ++++++++++++++++++++++++++++++ 7 files changed, 854 insertions(+), 2 deletions(-) create mode 100644 apisix/plugins/datadog.lua create mode 100644 docs/en/latest/plugins/datadog.md create mode 100644 t/lib/mock_dogstatsd.lua create mode 100644 t/plugin/datadog.t diff --git a/apisix/plugins/datadog.lua b/apisix/plugins/datadog.lua new file mode 100644 index 000000000000..7fe7d3f4e4e7 --- /dev/null +++ b/apisix/plugins/datadog.lua @@ -0,0 +1,277 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local core = require("apisix.core") +local plugin = require("apisix.plugin") +local batch_processor = require("apisix.utils.batch-processor") +local fetch_log = require("apisix.utils.log-util").get_full_log +local ngx = ngx +local udp = ngx.socket.udp +local format = string.format +local concat = table.concat +local buffers = {} +local ipairs = ipairs +local tostring = tostring +local stale_timer_running = false +local timer_at = ngx.timer.at + +local plugin_name = "datadog" +local defaults = { + host = "127.0.0.1", + port = 8125, + namespace = "apisix", + constant_tags = {"source:apisix"} +} + +local schema = { + type = "object", + properties = { + buffer_duration = {type = "integer", minimum = 1, default = 60}, + inactive_timeout = {type = "integer", minimum = 1, default = 5}, + batch_max_size = {type = "integer", minimum = 1, default = 5000}, + max_retry_count = {type = "integer", minimum = 1, default = 1}, + } +} + +local metadata_schema = { + type = "object", + properties = { + host = {type = "string", default= defaults.host}, + port = {type = "integer", minimum = 0, default = defaults.port}, + namespace = {type = "string", default = defaults.namespace}, + constant_tags = { + type = "array", + items = {type = "string"}, + default = defaults.constant_tags + } + }, +} + +local _M = { + version = 0.1, + priority = 495, + name = plugin_name, + schema = schema, + metadata_schema = metadata_schema, +} + +function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + end + return core.schema.check(schema, conf) +end + +local function generate_tag(entry, const_tags) + local tags + if const_tags and #const_tags > 0 then + tags = core.table.clone(const_tags) + else + tags = {} + end + + -- priority on route name, if not found using the route id. + if entry.route_name ~= "" then + core.table.insert(tags, "route_name:" .. entry.route_name) + elseif entry.route_id and entry.route_id ~= "" then + core.table.insert(tags, "route_name:" .. entry.route_id) + end + + if entry.service_id and entry.service_id ~= "" then + core.table.insert(tags, "service_id:" .. entry.service_id) + end + + if entry.consumer and entry.consumer ~= "" then + core.table.insert(tags, "consumer:" .. entry.consumer) + end + if entry.balancer_ip ~= "" then + core.table.insert(tags, "balancer_ip:" .. entry.balancer_ip) + end + if entry.response.status then + core.table.insert(tags, "response_status:" .. entry.response.status) + end + if entry.scheme ~= "" then + core.table.insert(tags, "scheme:" .. entry.scheme) + end + + if #tags > 0 then + return "|#" .. concat(tags, ',') + end + + return "" +end + +-- remove stale objects from the memory after timer expires +local function remove_stale_objects(premature) + if premature then + return + end + + for key, batch in ipairs(buffers) do + if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then + core.log.warn("removing batch processor stale object, conf: ", + core.json.delay_encode(key)) + buffers[key] = nil + end + end + + stale_timer_running = false +end + +function _M.log(conf, ctx) + + if not stale_timer_running then + -- run the timer every 30 mins if any log is present + timer_at(1800, remove_stale_objects) + stale_timer_running = true + end + + local entry = fetch_log(ngx, {}) + entry.upstream_latency = ctx.var.upstream_response_time * 1000 + entry.balancer_ip = ctx.balancer_ip or "" + entry.route_name = ctx.route_name or "" + entry.scheme = ctx.upstream_scheme or "" + + local log_buffer = buffers[conf] + if log_buffer then + log_buffer:push(entry) + return + end + + -- Generate a function to be executed by the batch processor + local func = function(entries, batch_max_size) + -- Fetching metadata details + local metadata = plugin.plugin_metadata(plugin_name) + if not metadata then + core.log.info("received nil metadata: using metadata defaults: ", + core.json.delay_encode(defaults, true)) + metadata = {} + metadata.value = defaults + end + + -- Creating a udp socket + local sock = udp() + local host, port = metadata.value.host, metadata.value.port + core.log.info("sending batch metrics to dogstatsd: ", host, ":", port) + + local ok, err = sock:setpeername(host, port) + + if not ok then + return false, "failed to connect to UDP server: host[" .. host + .. "] port[" .. tostring(port) .. "] err: " .. err + end + + -- Generate prefix & suffix according dogstatsd udp data format. + local prefix = metadata.value.namespace + if prefix ~= "" then + prefix = prefix .. "." + end + + core.log.info("datadog batch_entry: ", core.json.delay_encode(entries, true)) + for _, entry in ipairs(entries) do + local suffix = generate_tag(entry, metadata.value.constant_tags) + + -- request counter + local ok, err = sock:send(format("%s:%s|%s%s", prefix .. + "request.counter", 1, "c", suffix)) + if not ok then + core.log.error("failed to report request count to dogstatsd server: host[" .. host + .. "] port[" .. tostring(port) .. "] err: " .. err) + end + + + -- request latency histogram + local ok, err = sock:send(format("%s:%s|%s%s", prefix .. + "request.latency", entry.latency, "h", suffix)) + if not ok then + core.log.error("failed to report request latency to dogstatsd server: host[" + .. host .. "] port[" .. tostring(port) .. "] err: " .. err) + end + + -- upstream latency + local apisix_latency = entry.latency + if entry.upstream_latency then + local ok, err = sock:send(format("%s:%s|%s%s", prefix .. + "upstream.latency", entry.upstream_latency, "h", suffix)) + if not ok then + core.log.error("failed to report upstream latency to dogstatsd server: host[" + .. host .. "] port[" .. tostring(port) .. "] err: " .. err) + end + apisix_latency = apisix_latency - entry.upstream_latency + if apisix_latency < 0 then + apisix_latency = 0 + end + end + + -- apisix_latency + local ok, err = sock:send(format("%s:%s|%s%s", prefix .. + "apisix.latency", apisix_latency, "h", suffix)) + if not ok then + core.log.error("failed to report apisix latency to dogstatsd server: host[" .. host + .. "] port[" .. tostring(port) .. "] err: " .. err) + end + + -- request body size timer + local ok, err = sock:send(format("%s:%s|%s%s", prefix .. + "ingress.size", entry.request.size, "ms", suffix)) + if not ok then + core.log.error("failed to report req body size to dogstatsd server: host[" .. host + .. "] port[" .. tostring(port) .. "] err: " .. err) + end + + -- response body size timer + local ok, err = sock:send(format("%s:%s|%s%s", prefix .. + "egress.size", entry.response.size, "ms", suffix)) + if not ok then + core.log.error("failed to report response body size to dogstatsd server: host[" + .. host .. "] port[" .. tostring(port) .. "] err: " .. err) + end + end + + -- Releasing the UDP socket desciptor + ok, err = sock:close() + if not ok then + core.log.error("failed to close the UDP connection, host[", + host, "] port[", port, "] ", err) + end + + -- Returning at the end and ensuring the resource has been released. + return true + end + local config = { + name = plugin_name, + retry_delay = conf.retry_delay, + batch_max_size = conf.batch_max_size, + max_retry_count = conf.max_retry_count, + buffer_duration = conf.buffer_duration, + inactive_timeout = conf.inactive_timeout, + route_id = ctx.var.route_id, + server_addr = ctx.var.server_addr, + } + + local err + log_buffer, err = batch_processor:new(func, config) + + if not log_buffer then + core.log.error("error when creating the batch processor: ", err) + return + end + + buffers[conf] = log_buffer + log_buffer:push(entry) +end + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 236d50bc4cf0..922a91fc8e0d 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -341,6 +341,7 @@ plugins: # plugin list (sorted by priority) #- dubbo-proxy # priority: 507 - grpc-transcode # priority: 506 - prometheus # priority: 500 + - datadog # priority: 495 - echo # priority: 412 - http-logger # priority: 410 - sls-logger # priority: 406 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 7c88b9c3af7d..11a284b9d1d6 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -105,7 +105,8 @@ "plugins/prometheus", "plugins/zipkin", "plugins/skywalking", - "plugins/node-status" + "plugins/node-status", + "plugins/datadog" ] }, { diff --git a/docs/en/latest/plugins/datadog.md b/docs/en/latest/plugins/datadog.md new file mode 100644 index 000000000000..508ae1c5a2e8 --- /dev/null +++ b/docs/en/latest/plugins/datadog.md @@ -0,0 +1,131 @@ +--- +title: datadog +--- + + + +## Summary + +- [Summary](#summary) +- [Name](#name) +- [Attributes](#attributes) +- [Metadata](#metadata) +- [Exported Metrics](#exported-metrics) +- [How To Enable](#how-to-enable) +- [Disable Plugin](#disable-plugin) + +## Name + +`datadog` is a monitoring plugin built into Apache APISIX for seamless integration with [Datadog](https://www.datadoghq.com/), one of the most used monitoring and observability platform for cloud applications. If enabled, this plugin supports multiple powerful types of metrics capture for every request and response cycle that essentially reflects the behaviour and health of the system. + +This plugin pushes its custom metrics to the DogStatsD server, comes bundled with Datadog agent (to learn more about how to install a datadog agent, please visit [here](https://docs.datadoghq.com/agent/) ), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server. +To learn more about DogStatsD, please visit [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent) documentation. + +This plugin provides the ability to push metrics as a batch to the external Datadog agent, reusing the same datagram socket. In case if you did not receive the log data, don't worry give it some time. It will automatically send the logs after the timer function expires in our Batch Processor. + +For more info on Batch-Processor in Apache APISIX please refer. +[Batch-Processor](../batch-processor.md) + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | +| batch_max_size | integer | optional | 5000 | [1,...] | Max buffer size of each batch | +| inactive_timeout | integer | optional | 5 | [1,...] | Maximum age in seconds when the buffer will be flushed if inactive | +| buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed | +| max_retry_count | integer | optional | 1 | [1,...] | Maximum number of retries if one entry fails to reach dogstatsd server | + +## Metadata + +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------- | +| host | string | optional | "127.0.0.1" | | The DogStatsD server host address | +| port | integer | optional | 8125 | | The DogStatsD server host port | +| namespace | string | optional | "apisix" | | Prefix for all the custom metrics sent by APISIX agent. Useful for finding entities for metric graph. e.g. (apisix.request.counter) | +| constant_tags | array | optional | [ "source:apisix" ] | | Static tags embedded into generated metrics. Useful for grouping metric over certain signals. | + +To know more about how to effectively write tags, please visit [here](https://docs.datadoghq.com/getting_started/tagging/#defining-tags) + +## Exported Metrics + +Apache APISIX agent, for every request response cycle, export the following metrics to DogStatsD server if the datadog plugin is enabled: + +| Metric Name | StatsD Type | Description | +| ----------- | ----------- | ------- | +| Request Counter | Counter | No of requests received. | +| Request Latency | Histogram | Time taken to process the request (in milliseconds). | +| Upstream latency | Histogram | Time taken to proxy the request to the upstream server till a response is received (in milliseconds). | +| APISIX Latency | Histogram | Time taken by APISIX agent to process the request (in milliseconds). | +| Ingress Size | Timer | Request body size in bytes. | +| Egress Size | Timer | Response body size in bytes. | + +The metrics will be sent to the DogStatsD agent with the following tags: + +> If there is no suitable value for any particular tag, the tag will simply be omitted. + +- **route_name**: Name specified in the route schema definition. If not present, it will fall back to the route id value. + - Note: If multiple routes have the same name duplicated, we suggest you to visualize graphs on the Datadog dashboard over multiple tags that could compositely pinpoint a particular route/service. If it's still insufficient for your needs, feel free to drop a feature request at [apisix/issues](https://github.com/apache/apisix/issues). +- **service_id**: If a route has been created with the abstraction of service, the particular service id will be used. +- **consumer**: If the route has a linked consumer, the consumer Username will be added as a tag. +- **balancer_ip**: IP of the Upstream balancer that has processed the current request. +- **response_status**: HTTP response status code. +- **scheme**: Scheme that has been used to make the request. e.g. HTTP, gRPC, gRPCs etc. + +## How To Enable + +The following is an example on how to enable the datadog plugin for a specific route. We are assumming your datadog agent is already up an running. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "datadog": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +Now any requests to uri `/hello` will generate aforesaid metrics and push it to DogStatsD server of the datadog agent. + +## Disable Plugin + +Remove the corresponding json configuration in the plugin configuration to disable the `datadog`. +APISIX plugins are hot-reloaded, therefore no need to restart APISIX. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 2c67ef368228..cc95a649450a 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -40,7 +40,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ +qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ --- no_error_log [error] diff --git a/t/lib/mock_dogstatsd.lua b/t/lib/mock_dogstatsd.lua new file mode 100644 index 000000000000..f4ee675162fa --- /dev/null +++ b/t/lib/mock_dogstatsd.lua @@ -0,0 +1,45 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +local core = require("apisix.core") +local ngx = ngx +local socket = ngx.req.socket + +local _M = {} + +function _M.go() + local sock, err = socket() + if not sock then + core.log.error("failed to get the request socket: ", err) + return + end + + while true do + local data, err = sock:receive() + + if not data then + if err and err ~= "no more data" then + core.log.error("socket error, returning: ", err) + end + + return + else + core.log.warn("message received: ", data) + end + end +end + +return _M diff --git a/t/plugin/datadog.t b/t/plugin/datadog.t new file mode 100644 index 000000000000..b307ee7410a0 --- /dev/null +++ b/t/plugin/datadog.t @@ -0,0 +1,397 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + $block->set_value("stream_conf_enable", 1); + + if (!defined $block->extra_stream_config) { + my $stream_config = <<_EOC_; + server { + listen 8125 udp; + content_by_lua_block { + require("lib.mock_dogstatsd").go() + } + } +_EOC_ + $block->set_value("extra_stream_config", $stream_config); + } + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity check metadata +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local plugin = require("apisix.plugins.datadog") + local ok, err = plugin.check_schema({host = "127.0.0.1", port = 8125}, core.schema.TYPE_METADATA) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 2: add plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- setting the metadata + local code, meta_body = t('/apisix/admin/plugin_metadata/datadog', + ngx.HTTP_PUT, + [[{ + "host":"127.0.0.1", + "port": 8125 + }]], + [[{ + "node": { + "value": { + "namespace": "apisix", + "host": "127.0.0.1", + "constant_tags": [ + "source:apisix" + ], + "port": 8125 + }, + "key": "/apisix/plugin_metadata/datadog" + }, + "action": "set" + }]]) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "datadog": { + "batch_max_size" : 1, + "max_retry_count": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "name": "datadog", + "uri": "/opentracing" + }]], + [[{ + "node": { + "value": { + "plugins": { + "datadog": { + "batch_max_size": 1, + "max_retry_count": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing", + "name": "datadog" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + + ngx.say(meta_body) + ngx.say(body) + } + } +--- response_body +passed +passed + + + +=== TEST 3: testing behaviour with mock suite +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.print(body) + } + } +--- wait: 0.5 +--- response_body +opentracing +--- grep_error_log eval +qr/message received: apisix(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: apisix\.request\.counter:1\|c\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +/ + + + +=== TEST 4: testing behaviour with multiple requests +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.print(body) + + -- request 2 + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.print(body) + } + } +--- wait: 0.5 +--- response_body +opentracing +opentracing +--- grep_error_log eval +qr/message received: apisix(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: apisix\.request\.counter:1\|c\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.counter:1\|c\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +/ + + + +=== TEST 5: testing behaviour with different namespace +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + -- Change the metadata + local code, meta_body = t('/apisix/admin/plugin_metadata/datadog', + ngx.HTTP_PUT, + [[{ + "host":"127.0.0.1", + "port": 8125, + "namespace": "mycompany" + }]]) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.say(meta_body) + + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.print(body) + } + } +--- wait: 0.5 +--- response_body +passed +opentracing +--- grep_error_log eval +qr/message received: mycompany(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: mycompany\.request\.counter:1\|c\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: mycompany\.request\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: mycompany\.upstream\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: mycompany\.apisix\.latency:[\d.]+\|h\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: mycompany\.ingress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: mycompany\.egress\.size:[\d]+\|ms\|#source:apisix,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +/ + + + +=== TEST 6: testing behaviour with different constant tags +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + -- Change the metadata + local code, meta_body = t('/apisix/admin/plugin_metadata/datadog', + ngx.HTTP_PUT, + [[{ + "host":"127.0.0.1", + "port": 8125, + "constant_tags": [ + "source:apisix", + "new_tag:must" + ] + }]]) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.say(meta_body) + + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.print(body) + } + } +--- wait: 0.5 +--- response_body +passed +opentracing +--- grep_error_log eval +qr/message received: apisix(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: apisix\.request\.counter:1\|c\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:datadog,balancer_ip:[\d.]+,response_status:200,scheme:http +/ + + + +=== TEST 7: testing behaviour when route_name is missing - must fallback to route_id +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "datadog": { + "batch_max_size" : 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + -- making a request to the route + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.print(body) + } + } +--- response_body +passed +opentracing +--- wait: 0.5 +--- grep_error_log eval +qr/message received: apisix(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: apisix\.request\.counter:1\|c\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +/ From 094282f12a4a4a60429f1baae7fe097000833a5d Mon Sep 17 00:00:00 2001 From: Wen Ming Date: Tue, 9 Nov 2021 11:07:36 +0800 Subject: [PATCH 078/260] docs: add Datadog in README.md. (#5452) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index be4b386a8bee..2e4c114c3435 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - [Fault Injection](docs/en/latest/plugins/fault-injection.md) - [REST Admin API](docs/en/latest/admin-api.md): Using the REST Admin API to control Apache APISIX, which only allows 127.0.0.1 access by default, you can modify the `allow_admin` field in `conf/config.yaml` to specify a list of IPs that are allowed to call the Admin API. Also, note that the Admin API uses key auth to verify the identity of the caller. **The `admin_key` field in `conf/config.yaml` needs to be modified before deployment to ensure security**. - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md)) + - [Datadog](docs/en/latest/plugins/datadog.md): push custom metrics to the DogStatsD server, comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server. - [Helm charts](https://github.com/apache/apisix-helm-chart) - **Highly scalable** From d16aa8ec9b7d1a7745caa81d3ca07b7623121cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 9 Nov 2021 15:45:01 +0800 Subject: [PATCH 079/260] docs: prepare for the upcoming Faas plugins (#5455) --- docs/en/latest/config.json | 8 +++++++- docs/zh/latest/config.json | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 11a284b9d1d6..89236d3a17b9 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -35,7 +35,6 @@ "label": "General", "items": [ "plugins/batch-requests", - "plugins/serverless", "plugins/redirect", "plugins/echo", "plugins/gzip", @@ -123,6 +122,13 @@ "plugins/sls-logger" ] }, + { + "type": "category", + "label": "Serverless", + "items": [ + "plugins/serverless" + ] + }, { "type": "category", "label": "Other Protocols", diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 986a74ffcbf4..f2f863d0ffc7 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -35,7 +35,6 @@ "label": "General", "items": [ "plugins/batch-requests", - "plugins/serverless", "plugins/redirect", "plugins/echo", "plugins/gzip", @@ -121,6 +120,13 @@ "plugins/sls-logger" ] }, + { + "type": "category", + "label": "Serverless", + "items": [ + "plugins/serverless" + ] + }, { "type": "category", "label": "其它", From 9fc38330e82ce46e2aaabceef7d61708c91782db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 9 Nov 2021 16:21:27 +0800 Subject: [PATCH 080/260] fix: prevent being hacked by untrusted request_uri (#5458) Thanks to Marcin Niemiec for the report. Signed-off-by: spacewander --- apisix/core/ctx.lua | 8 +++++++- apisix/init.lua | 6 ++++++ t/plugin/uri-blocker.t | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua index fb77a378dd9d..30c764432e0f 100644 --- a/apisix/core/ctx.lua +++ b/apisix/core/ctx.lua @@ -119,6 +119,12 @@ do end } + local no_cacheable_var_names = { + -- var.args should not be cached as it can be changed via set_uri_args + args = true, + is_args = true, + } + local ngx_var_names = { upstream_scheme = true, upstream_host = true, @@ -224,7 +230,7 @@ do val = get_var(key, t._request) end - if val ~= nil then + if val ~= nil and not no_cacheable_var_names[key] then t._cache[key] = val end diff --git a/apisix/init.lua b/apisix/init.lua index e875bb5213fd..5ef855a3c5c2 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -367,6 +367,12 @@ function _M.http_access_phase() end end + -- To prevent being hacked by untrusted request_uri, here we + -- record the normalized but not rewritten uri as request_uri, + -- the original request_uri can be accessed via var.real_request_uri + api_ctx.var.real_request_uri = api_ctx.var.request_uri + api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or "") + if router.api.has_route_not_under_apisix() or core.string.has_prefix(uri, "/apisix/") then diff --git a/t/plugin/uri-blocker.t b/t/plugin/uri-blocker.t index 0d0bce8a36f5..2aee13e537e2 100644 --- a/t/plugin/uri-blocker.t +++ b/t/plugin/uri-blocker.t @@ -485,3 +485,39 @@ GET /hello?aa=1 {"error_msg":"access is not allowed"} --- no_error_log [error] + + + +=== TEST 21: add block rule with anchor +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "uri-blocker": { + "block_rules": ["^/internal/"] + } + }, + "uri": "/internal/*" + }]]) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } +} +--- request +GET /t + + + +=== TEST 22: can't bypass with url without normalization +--- request +GET /./internal/x?aa=1 +--- error_code: 403 +--- no_error_log +[error] From 8bb5adcb66b69c1deab6dd53b4bec19c28b0fd7a Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 10 Nov 2021 06:51:57 +0530 Subject: [PATCH 081/260] test: introducing ---exec & --- stdin section to run cmd (#5460) --- t/APISIX.pm | 32 +++++++++++++++ t/node/grpc-proxy-mtls.t | 40 ++----------------- t/node/grpc-proxy-stream.t | 63 +++--------------------------- t/node/grpc-proxy-unary.t | 79 ++++---------------------------------- 4 files changed, 50 insertions(+), 164 deletions(-) diff --git a/t/APISIX.pm b/t/APISIX.pm index 1f0f18a7f947..43dbf92eacab 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -301,6 +301,38 @@ _EOC_ $block->set_value("config", $config) } + # handling shell exec in test Nginx + my $exec_snippet = $block->exec; + if ($exec_snippet) { + # capture the stdin & max response size + my $stdin = "nil"; + if ($block->stdin) { + $stdin = '"' . $block->stdin . '"'; + } + chomp $exec_snippet; + chomp $stdin; + + my $max_size = $block->max_size // 8096; + $block->set_value("request", "GET /exec_request"); + + my $config = $block->config // ''; + $config .= <<_EOC_; + location /exec_request { + content_by_lua_block { + local shell = require("resty.shell") + local ok, stdout, stderr, reason, status = shell.run([[ $exec_snippet ]], $stdin, @{[$timeout*1000]}, $max_size) + if not ok then + ngx.log(ngx.WARN, "failed to execute the script with status: " .. status .. ", reason: " .. reason .. ", stderr: " .. stderr) + return + end + ngx.print(stdout) + } + } +_EOC_ + + $block->set_value("config", $config) + } + my $stream_enable = $block->stream_enable; my $stream_conf_enable = $block->stream_conf_enable; my $extra_stream_config = $block->extra_stream_config // ''; diff --git a/t/node/grpc-proxy-mtls.t b/t/node/grpc-proxy-mtls.t index 864099341b7a..3c974fed1cbb 100644 --- a/t/node/grpc-proxy-mtls.t +++ b/t/node/grpc-proxy-mtls.t @@ -62,24 +62,8 @@ routes: "127.0.0.1:50053": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - return - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHello --- response_body { "message": "Hello apisix" @@ -107,24 +91,8 @@ routes: "127.0.0.1:50053": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - return - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream --- response_body { "message": "Hello apisix" diff --git a/t/node/grpc-proxy-stream.t b/t/node/grpc-proxy-stream.t index 21100d88bc47..1f10b9aad592 100644 --- a/t/node/grpc-proxy-stream.t +++ b/t/node/grpc-proxy-stream.t @@ -50,27 +50,8 @@ routes: "127.0.0.1:50051": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local opts = { - merge_stderr = true - } - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloServerStream", opts) - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - return - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloServerStream --- response_body { "message": "Hello apisix" @@ -107,24 +88,8 @@ routes: "127.0.0.1:50051": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloClientStream") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - return - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"} {"name":"apisix"} {"name":"apisix"} {"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloClientStream --- response_body { "message": "Hello apisix!Hello apisix!Hello apisix!Hello apisix!" @@ -149,24 +114,8 @@ routes: "127.0.0.1:50051": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"} {\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - return - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"} {"name":"apisix"} {"name":"apisix"} {"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHelloBidirectionalStream --- response_body { "message": "Hello apisix" diff --git a/t/node/grpc-proxy-unary.t b/t/node/grpc-proxy-unary.t index 330b9b44a2da..393016d1578f 100644 --- a/t/node/grpc-proxy-unary.t +++ b/t/node/grpc-proxy-unary.t @@ -50,26 +50,8 @@ routes: "127.0.0.1:50051": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local opts= { - merge_stderr = true, - } - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello", opts) - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHello --- response_body { "message": "Hello apisix" @@ -94,23 +76,8 @@ routes: "127.0.0.1:50051": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHello --- response_body { "message": "Hello apisix" @@ -135,23 +102,8 @@ routes: "127.0.0.1:50052": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{\"name\":\"apisix\"}' 127.0.0.1:1984 helloworld.Greeter.SayHello") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -plaintext -d '{"name":"apisix"}' 127.0.0.1:1984 helloworld.Greeter.SayHello --- response_body { "message": "Hello apisix" @@ -182,23 +134,8 @@ routes: "127.0.0.1:50051": 1 type: roundrobin #END ---- config - location /t { - content_by_lua_block { - local ngx_pipe = require("ngx.pipe") - local proc, err = ngx_pipe.spawn("grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -insecure -d '{\"name\":\"apisix\"}' test.com:1994 helloworld.Greeter.SayHello") - if not proc then - ngx.say(err) - return - end - local data, err = proc:stdout_read_all() - if not data then - ngx.say(err) - return - end - ngx.say(data:sub(1, -2)) - } - } +--- exec +grpcurl -import-path ./t/grpc_server_example/proto -proto helloworld.proto -insecure -d '{"name":"apisix"}' test.com:1994 helloworld.Greeter.SayHello --- response_body { "message": "Hello apisix" From bb75ed6f40b90888dd755dfe7b5699febdae6ad3 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Tue, 9 Nov 2021 19:25:38 -0600 Subject: [PATCH 082/260] chore: remove legacy code (#5454) --- apisix/init.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apisix/init.lua b/apisix/init.lua index 5ef855a3c5c2..801809f64b10 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -716,10 +716,6 @@ function _M.http_log_phase() api_ctx.server_picker.after_balance(api_ctx, false) end - if api_ctx.uri_parse_param then - core.tablepool.release("uri_parse_param", api_ctx.uri_parse_param) - end - core.ctx.release_vars(api_ctx) if api_ctx.plugins then core.tablepool.release("plugins", api_ctx.plugins) From e94c6eef587887eb3a078aea3a74d5021b0dde65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 10 Nov 2021 09:31:12 +0800 Subject: [PATCH 083/260] chore: clean up outdated comments (#5459) --- apisix/core/request.lua | 2 -- apisix/plugins/example-plugin.lua | 2 +- apisix/plugins/limit-req.lua | 2 +- apisix/plugins/proxy-rewrite.lua | 2 -- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apisix/core/request.lua b/apisix/core/request.lua index d5482e6db72c..f098f63280b3 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -198,8 +198,6 @@ end function _M.get_body(max_size, ctx) - -- TODO: improve the check with set client_max_body dynamically - -- which requires to change Nginx source code if max_size then local var = ctx and ctx.var or ngx.var local content_length = tonumber(var.http_content_length) diff --git a/apisix/plugins/example-plugin.lua b/apisix/plugins/example-plugin.lua index f1d1cc33c625..07e8024d7a81 100644 --- a/apisix/plugins/example-plugin.lua +++ b/apisix/plugins/example-plugin.lua @@ -44,7 +44,7 @@ local plugin_name = "example-plugin" local _M = { version = 0.1, - priority = 0, -- TODO: add a type field, may be a good idea + priority = 0, name = plugin_name, schema = schema, metadata_schema = metadata_schema, diff --git a/apisix/plugins/limit-req.lua b/apisix/plugins/limit-req.lua index 824a47d4ed12..6439bf5bb6cb 100644 --- a/apisix/plugins/limit-req.lua +++ b/apisix/plugins/limit-req.lua @@ -51,7 +51,7 @@ local schema = { local _M = { version = 0.1, - priority = 1001, -- TODO: add a type field, may be a good idea + priority = 1001, name = plugin_name, schema = schema, } diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua index ca9210e14146..0780b211dab6 100644 --- a/apisix/plugins/proxy-rewrite.lua +++ b/apisix/plugins/proxy-rewrite.lua @@ -199,8 +199,6 @@ function _M.rewrite(conf, ctx) return end - -- reform header from object into array, so can avoid use pairs, - -- which is NYI if not conf.headers_arr then conf.headers_arr = {} From 1514fe48c349534a88fda669ac2ecda8ee31f27c Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Wed, 10 Nov 2021 01:48:05 -0600 Subject: [PATCH 084/260] fix(hmac-auth): check if the X-HMAC-ALGORITHM header is missing (#5467) --- apisix/plugins/hmac-auth.lua | 4 +++ t/plugin/hmac-auth.t | 49 +++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/apisix/plugins/hmac-auth.lua b/apisix/plugins/hmac-auth.lua index 35a11bd24a81..73cb947d3c03 100644 --- a/apisix/plugins/hmac-auth.lua +++ b/apisix/plugins/hmac-auth.lua @@ -291,6 +291,10 @@ local function validate(ctx, params) return nil, {message = "access key or signature missing"} end + if not params.algorithm then + return nil, {message = "algorithm missing"} + end + local consumer, err = get_consumer(params.access_key) if err then return nil, err diff --git a/t/plugin/hmac-auth.t b/t/plugin/hmac-auth.t index cc029db86d6b..4de139b39e71 100644 --- a/t/plugin/hmac-auth.t +++ b/t/plugin/hmac-auth.t @@ -241,7 +241,22 @@ GET /hello -=== TEST 8: verify: invalid access key +=== TEST 8: verify, missing algorithm +--- request +GET /hello +--- more_headers +X-HMAC-SIGNATURE: asdf +Date: Thu, 24 Sep 2020 06:39:52 GMT +X-HMAC-ACCESS-KEY: my-access-key +--- error_code: 401 +--- response_body +{"message":"algorithm missing"} +--- no_error_log +[error] + + + +=== TEST 9: verify: invalid access key --- request GET /hello --- more_headers @@ -257,7 +272,7 @@ X-HMAC-ACCESS-KEY: sdf -=== TEST 9: verify: invalid algorithm +=== TEST 10: verify: invalid algorithm --- request GET /hello --- more_headers @@ -273,7 +288,7 @@ X-HMAC-ACCESS-KEY: my-access-key -=== TEST 10: verify: Clock skew exceeded +=== TEST 11: verify: Clock skew exceeded --- request GET /hello --- more_headers @@ -289,7 +304,7 @@ X-HMAC-ACCESS-KEY: my-access-key -=== TEST 11: verify: missing Date +=== TEST 12: verify: missing Date --- request GET /hello --- more_headers @@ -304,7 +319,7 @@ X-HMAC-ACCESS-KEY: my-access-key -=== TEST 12: verify: Invalid GMT format time +=== TEST 13: verify: Invalid GMT format time --- request GET /hello --- more_headers @@ -320,7 +335,7 @@ X-HMAC-ACCESS-KEY: my-access-key -=== TEST 13: verify: ok +=== TEST 14: verify: ok --- config location /t { content_by_lua_block { @@ -381,7 +396,7 @@ passed -=== TEST 14: add consumer with 0 clock skew +=== TEST 15: add consumer with 0 clock skew --- config location /t { content_by_lua_block { @@ -429,7 +444,7 @@ passed -=== TEST 15: verify: invalid signature +=== TEST 16: verify: invalid signature --- request GET /hello --- more_headers @@ -445,7 +460,7 @@ X-HMAC-ACCESS-KEY: my-access-key3 -=== TEST 16: add consumer with 1 clock skew +=== TEST 17: add consumer with 1 clock skew --- config location /t { content_by_lua_block { @@ -493,7 +508,7 @@ passed -=== TEST 17: verify: Invalid GMT format time +=== TEST 18: verify: Invalid GMT format time --- config location /t { content_by_lua_block { @@ -548,7 +563,7 @@ qr/\{"message":"Clock skew exceeded"\}/ -=== TEST 18: verify: put ok +=== TEST 19: verify: put ok --- config location /t { content_by_lua_block { @@ -613,7 +628,7 @@ passed -=== TEST 19: verify: put ok (pass auth data by header `Authorization`) +=== TEST 20: verify: put ok (pass auth data by header `Authorization`) --- config location /t { content_by_lua_block { @@ -677,7 +692,7 @@ passed -=== TEST 20: hit route without auth info +=== TEST 21: hit route without auth info --- request GET /hello --- error_code: 401 @@ -688,7 +703,7 @@ GET /hello -=== TEST 21: add consumer with signed_headers +=== TEST 22: add consumer with signed_headers --- config location /t { content_by_lua_block { @@ -737,7 +752,7 @@ passed -=== TEST 22: verify with invalid signed header +=== TEST 23: verify with invalid signed header --- config location /t { content_by_lua_block { @@ -790,7 +805,7 @@ qr/\{"message":"Invalid signed header x-custom-header-c"\}/ -=== TEST 23: verify ok with signed headers +=== TEST 24: verify ok with signed headers --- config location /t { content_by_lua_block { @@ -847,7 +862,7 @@ passed -=== TEST 24: add consumer with plugin hmac-auth - empty configuration +=== TEST 25: add consumer with plugin hmac-auth - empty configuration --- config location /t { content_by_lua_block { From 593f00ff57b0bb5ff124aa974927004c5e7195e6 Mon Sep 17 00:00:00 2001 From: Xu_Mj <40650924+Xu-Mj@users.noreply.github.com> Date: Thu, 11 Nov 2021 08:54:06 +0800 Subject: [PATCH 085/260] docs: update limit-req.md (#5461) --- docs/zh/latest/plugins/limit-req.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index a7d88ca7f827..58ffaac4c281 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -38,13 +38,12 @@ title: limit-req | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ------------- | ------- | ------ | ------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| rate | integer | 必须 | | rate > 0 | 指定的请求速率(以秒为单位),请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求会被加上延时。 | -| burst | integer | 必须 | | burst >= 0 | 请求速率超过 (`rate` + `brust`)的请求会被直接拒绝。 | -| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | -| key | string | 必须 | | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称,如 "remote_addr" 和 "consumer_name"。如果 `key_type` 为 "var_combination",那么 key 会当作变量组合,如 "$remote_addr $consumer_name"。如果 key 的值为空,$remote_addr 会被作为默认 key。 | +| rate | integer | 必须 | | rate > 0 | 指定的请求速率(以秒为单位),请求速率超过 `rate` 但没有超过 (`rate` + `burst`)的请求会被加上延时。 | +| burst | integer | 必须 | | burst >= 0 | t请求速率超过 (`rate` + `burst`)的请求会被直接拒绝。 | +| key | string | 必须 | | ["remote_addr", "server_addr", "http_x_real_ip", "http_x_forwarded_for", "consumer_name"] | 用来做请求计数的依据,当前接受的 key 有:"remote_addr"(客户端IP地址), "server_addr"(服务端 IP 地址), 请求头中的"X-Forwarded-For" 或 "X-Real-IP","consumer_name"(consumer 的 username)。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码。 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | -| nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `brust`)的请求不会加上延迟, 如果是 false,则会加上延迟。 | +| nodelay | boolean | 可选 | false | | 如果 nodelay 为 true, 请求速率超过 `rate` 但没有超过 (`rate` + `burst`)的请求不会加上延迟, 如果是 false,则会加上延迟。 | | allow_degradation | boolean | 可选 | false | | 当限速插件功能临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| ## 示例 From f06f6cc0e8e41875bf105cf2c18457339002df53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 11 Nov 2021 09:27:58 +0800 Subject: [PATCH 086/260] fix(upstream): load imbalance when it's referred by multiple routes (#5462) --- apisix/upstream.lua | 2 +- t/node/least_conn2.t | 109 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 t/node/least_conn2.t diff --git a/apisix/upstream.lua b/apisix/upstream.lua index 8c4919a179df..4e23fbffa986 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -273,7 +273,7 @@ function _M.set_by_route(route, api_ctx) end set_directly(api_ctx, up_conf.type .. "#upstream_" .. tostring(up_conf), - api_ctx.conf_version, up_conf) + tostring(up_conf), up_conf) local nodes_count = up_conf.nodes and #up_conf.nodes or 0 if nodes_count == 0 then diff --git a/t/node/least_conn2.t b/t/node/least_conn2.t new file mode 100644 index 000000000000..1141ab5fee05 --- /dev/null +++ b/t/node/least_conn2.t @@ -0,0 +1,109 @@ +# +# 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(2); +log_level('info'); +no_root_location(); +worker_connections(1024); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: upstream across multiple routes should not share the same version +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "type": "least_conn", + "nodes": { + "127.0.0.1:1980": 3, + "0.0.0.0:1980": 2 + } + }]] + ) + assert(code < 300, body) + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "host": "1.com", + "uri": "/mysleep", + "upstream_id": "1" + }]] + ) + assert(code < 300, body) + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "host": "2.com", + "uri": "/mysleep", + "upstream_id": "1" + }]] + ) + assert(code < 300, body) + } + } + + + +=== TEST 2: hit +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/mysleep?seconds=0.1" + + local t = {} + for i = 1, 2 do + local th = assert(ngx.thread.spawn(function(i) + local httpc = http.new() + local res, err = httpc:request_uri(uri, {headers = {Host = i..".com"}}) + if not res then + ngx.log(ngx.ERR, err) + return + end + end, i)) + table.insert(t, th) + end + for i, th in ipairs(t) do + ngx.thread.wait(th) + end + } + } +--- grep_error_log eval +qr/proxy request to \S+ while connecting to upstream/ +--- grep_error_log_out +proxy request to 127.0.0.1:1980 while connecting to upstream +proxy request to 0.0.0.0:1980 while connecting to upstream From 7b60f42785dda9fcd22bf67af3dfac4aca0f9ac8 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Thu, 11 Nov 2021 07:40:30 +0530 Subject: [PATCH 087/260] feat: introducing prefer_name attribute in datadog plugin (#5463) --- apisix/plugins/datadog.lua | 25 ++++-- docs/en/latest/plugins/datadog.md | 20 ++--- t/plugin/datadog.t | 138 ++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 16 deletions(-) diff --git a/apisix/plugins/datadog.lua b/apisix/plugins/datadog.lua index 7fe7d3f4e4e7..0a6a134bcff2 100644 --- a/apisix/plugins/datadog.lua +++ b/apisix/plugins/datadog.lua @@ -18,6 +18,7 @@ local core = require("apisix.core") local plugin = require("apisix.plugin") local batch_processor = require("apisix.utils.batch-processor") local fetch_log = require("apisix.utils.log-util").get_full_log +local service_fetch = require("apisix.http.service").get local ngx = ngx local udp = ngx.socket.udp local format = string.format @@ -43,6 +44,7 @@ local schema = { inactive_timeout = {type = "integer", minimum = 1, default = 5}, batch_max_size = {type = "integer", minimum = 1, default = 5000}, max_retry_count = {type = "integer", minimum = 1, default = 1}, + prefer_name = {type = "boolean", default = true} } } @@ -83,15 +85,12 @@ local function generate_tag(entry, const_tags) tags = {} end - -- priority on route name, if not found using the route id. - if entry.route_name ~= "" then - core.table.insert(tags, "route_name:" .. entry.route_name) - elseif entry.route_id and entry.route_id ~= "" then + if entry.route_id and entry.route_id ~= "" then core.table.insert(tags, "route_name:" .. entry.route_id) end if entry.service_id and entry.service_id ~= "" then - core.table.insert(tags, "service_id:" .. entry.service_id) + core.table.insert(tags, "service_name:" .. entry.service_id) end if entry.consumer and entry.consumer ~= "" then @@ -142,9 +141,23 @@ function _M.log(conf, ctx) local entry = fetch_log(ngx, {}) entry.upstream_latency = ctx.var.upstream_response_time * 1000 entry.balancer_ip = ctx.balancer_ip or "" - entry.route_name = ctx.route_name or "" entry.scheme = ctx.upstream_scheme or "" + -- if prefer_name is set, fetch the service/route name. If the name is nil, fall back to id. + if conf.prefer_name then + if entry.service_id and entry.service_id ~= "" then + local svc = service_fetch(entry.service_id) + + if svc and svc.value.name ~= "" then + entry.service_id = svc.value.name + end + end + + if ctx.route_name and ctx.route_name ~= "" then + entry.route_id = ctx.route_name + end + end + local log_buffer = buffers[conf] if log_buffer then log_buffer:push(entry) diff --git a/docs/en/latest/plugins/datadog.md b/docs/en/latest/plugins/datadog.md index 508ae1c5a2e8..7842018eb182 100644 --- a/docs/en/latest/plugins/datadog.md +++ b/docs/en/latest/plugins/datadog.md @@ -45,12 +45,13 @@ For more info on Batch-Processor in Apache APISIX please refer. ## Attributes -| Name | Type | Requirement | Default | Valid | Description | -| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | -| batch_max_size | integer | optional | 5000 | [1,...] | Max buffer size of each batch | -| inactive_timeout | integer | optional | 5 | [1,...] | Maximum age in seconds when the buffer will be flushed if inactive | -| buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed | -| max_retry_count | integer | optional | 1 | [1,...] | Maximum number of retries if one entry fails to reach dogstatsd server | +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | +| prefer_name | boolean | optional | true | true/false | If set to `false`, would use route/service id instead of name(default) with metric tags. | +| batch_max_size | integer | optional | 5000 | [1,...] | Max buffer size of each batch | +| inactive_timeout | integer | optional | 5 | [1,...] | Maximum age in seconds when the buffer will be flushed if inactive | +| buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed | +| max_retry_count | integer | optional | 1 | [1,...] | Maximum number of retries if one entry fails to reach dogstatsd server | ## Metadata @@ -80,13 +81,12 @@ The metrics will be sent to the DogStatsD agent with the following tags: > If there is no suitable value for any particular tag, the tag will simply be omitted. -- **route_name**: Name specified in the route schema definition. If not present, it will fall back to the route id value. - - Note: If multiple routes have the same name duplicated, we suggest you to visualize graphs on the Datadog dashboard over multiple tags that could compositely pinpoint a particular route/service. If it's still insufficient for your needs, feel free to drop a feature request at [apisix/issues](https://github.com/apache/apisix/issues). -- **service_id**: If a route has been created with the abstraction of service, the particular service id will be used. +- **route_name**: Name specified in the route schema definition. If not present or plugin attribute `prefer_name` is set to `false`, it will fall back to the route id value. +- **service_name**: If a route has been created with the abstraction of service, the particular service name/id (based on plugin `prefer_name` attribute) will be used. - **consumer**: If the route has a linked consumer, the consumer Username will be added as a tag. - **balancer_ip**: IP of the Upstream balancer that has processed the current request. - **response_status**: HTTP response status code. -- **scheme**: Scheme that has been used to make the request. e.g. HTTP, gRPC, gRPCs etc. +- **scheme**: Scheme that has been used to make the request, such as HTTP, gRPC, gRPCs etc. ## How To Enable diff --git a/t/plugin/datadog.t b/t/plugin/datadog.t index b307ee7410a0..74dafedd1fa4 100644 --- a/t/plugin/datadog.t +++ b/t/plugin/datadog.t @@ -395,3 +395,141 @@ message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,new_tag:must message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http / + + + +=== TEST 8: testing behaviour with service id +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "name": "service-1", + "plugins": { + "datadog": { + "batch_max_size" : 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + -- create a route with service level abstraction + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "name": "route-1", + "uri": "/opentracing", + "service_id": "1" + + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + -- making a request to the route + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.print(body) + } + } +--- response_body +passed +passed +opentracing +--- wait: 0.5 +--- grep_error_log eval +qr/message received: apisix(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: apisix\.request\.counter:1\|c\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:route-1,service_name:service-1,balancer_ip:[\d.]+,response_status:200,scheme:http +/ + + + +=== TEST 9: testing behaviour with prefer_name is false and service name is nil +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "datadog": { + "batch_max_size" : 1, + "prefer_name": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + -- making a request to the route + local code, _, body = t("/opentracing", "GET") + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.print(body) + } + } +--- response_body +passed +opentracing +--- wait: 0.5 +--- grep_error_log eval +qr/message received: apisix(.+?(?=, ))/ +--- grep_error_log_out eval +qr/message received: apisix\.request\.counter:1\|c\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.request\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.upstream\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.apisix\.latency:[\d.]+\|h\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.ingress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +message received: apisix\.egress\.size:[\d]+\|ms\|#source:apisix,new_tag:must,route_name:1,service_name:1,balancer_ip:[\d.]+,response_status:200,scheme:http +/ From 0aa97c30f7741eeeb341eed0795c3bd0ef1b7556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 11 Nov 2021 11:25:36 +0800 Subject: [PATCH 088/260] fix(batch-requests): correct the client ip in the pipeline (#5476) --- apisix/cli/ops.lua | 20 ++++++++++++++++++++ apisix/plugins/batch-requests.lua | 6 ++++++ docs/en/latest/plugins/batch-requests.md | 9 ++++++++- docs/zh/latest/plugins/batch-requests.md | 9 ++++++++- t/cli/test_validate_config.sh | 17 +++++++++++++++++ t/plugin/batch-requests.t | 15 +++++++++++++++ t/plugin/batch-requests2.t | 7 +++++++ 7 files changed, 81 insertions(+), 2 deletions(-) diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index ed9abe92556a..4c68ef38fadb 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -482,6 +482,26 @@ Please modify "admin_key" in conf/config.yaml . util.die("missing apisix.proxy_cache for plugin proxy-cache\n") end + if enabled_plugins["batch-requests"] then + local pass_real_client_ip = false + local real_ip_from = yaml_conf.nginx_config.http.real_ip_from + -- the real_ip_from is enabled by default, we just need to make sure it's + -- not disabled by the users + if real_ip_from then + for _, ip in ipairs(real_ip_from) do + -- TODO: handle cidr + if ip == "127.0.0.1" or ip == "0.0.0.0/0" then + pass_real_client_ip = true + end + end + end + + if not pass_real_client_ip then + util.die("missing '127.0.0.1' in the nginx_config.http.real_ip_from for plugin " .. + "batch-requests\n") + end + end + local ports_to_check = {} -- listen in admin use a separate port, support specific IP, compatible with the original style diff --git a/apisix/plugins/batch-requests.lua b/apisix/plugins/batch-requests.lua index 73c129f9c630..71a37524d42f 100644 --- a/apisix/plugins/batch-requests.lua +++ b/apisix/plugins/batch-requests.lua @@ -162,6 +162,10 @@ end local function set_common_header(data) + local local_conf = core.config.local_conf() + local real_ip_hdr = core.table.try_read_attr(local_conf, "nginx_config", "http", + "real_ip_header") + local outer_headers = core.request.headers(nil) for i,req in ipairs(data.pipeline) do for k, v in pairs(data.headers) do @@ -179,6 +183,8 @@ local function set_common_header(data) end end end + + req.headers[real_ip_hdr] = core.request.get_remote_client_ip() end end diff --git a/docs/en/latest/plugins/batch-requests.md b/docs/en/latest/plugins/batch-requests.md index dff9d41f8a31..7e258eb29f75 100644 --- a/docs/en/latest/plugins/batch-requests.md +++ b/docs/en/latest/plugins/batch-requests.md @@ -51,7 +51,14 @@ You may need to use [interceptors](../plugin-interceptors.md) to protect it. ## How To Enable -Default enabled +Enable the batch-requests plugin in the `config.yaml`: + +``` +# Add this in config.yaml +plugins: + - ... # plugin you need + - batch-requests +``` ## How To Configure diff --git a/docs/zh/latest/plugins/batch-requests.md b/docs/zh/latest/plugins/batch-requests.md index 325ab5050f41..538ac8e79bce 100644 --- a/docs/zh/latest/plugins/batch-requests.md +++ b/docs/zh/latest/plugins/batch-requests.md @@ -57,7 +57,14 @@ title: batch-requests ## 如何启用 -本插件默认启用。 +你需要在 `config.yaml` 里面启用 batch-requests 插件: + +``` +# 加到 config.yaml +plugins: + - ... # plugin you need + - batch-requests +``` ## 如何配置 diff --git a/t/cli/test_validate_config.sh b/t/cli/test_validate_config.sh index c2c7b693af73..8b562d236915 100755 --- a/t/cli/test_validate_config.sh +++ b/t/cli/test_validate_config.sh @@ -135,3 +135,20 @@ if echo "$out" | grep "\[emerg\] unknown directive \"notexist\""; then fi echo "passed: apisix test(failure scenario)" + +echo ' +plugins: +- batch-requests +nginx_config: + http: + real_ip_from: + - "127.0.0.2" +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep "missing '127.0.0.1' in the nginx_config.http.real_ip_from for plugin batch-requests"; then + echo "failed: should check the realip configuration for batch-requests" + exit 1 +fi + +echo "passed: check the realip configuration for batch-requests" diff --git a/t/plugin/batch-requests.t b/t/plugin/batch-requests.t index e341b87669e0..415369ea76fd 100644 --- a/t/plugin/batch-requests.t +++ b/t/plugin/batch-requests.t @@ -21,6 +21,17 @@ no_long_string(); no_root_location(); log_level("debug"); +add_block_preprocessor(sub { + my ($block) = @_; + + my $extra_yaml_config = <<_EOC_; +plugins: + - batch-requests +_EOC_ + + $block->set_value("extra_yaml_config", $extra_yaml_config); +}); + run_tests; __DATA__ @@ -67,6 +78,7 @@ __DATA__ "status": 200, "body":"B", "headers": { + "Client-IP": "127.0.0.1", "Base-Header": "base", "Base-Query": "base_query", "X-Res": "B", @@ -80,6 +92,7 @@ __DATA__ "status": 201, "body":"C", "headers": { + "Client-IP-From-Hdr": "127.0.0.1", "Base-Header": "base", "Base-Query": "base_query", "X-Res": "C", @@ -111,6 +124,7 @@ __DATA__ location = /b { content_by_lua_block { ngx.status = 200 + ngx.header["Client-IP"] = ngx.var.remote_addr ngx.header["Base-Header"] = ngx.req.get_headers()["Base-Header"] ngx.header["Base-Query"] = ngx.var.arg_base ngx.header["X-Header1"] = ngx.req.get_headers()["Header1"] @@ -124,6 +138,7 @@ __DATA__ location = /c { content_by_lua_block { ngx.status = 201 + ngx.header["Client-IP-From-Hdr"] = ngx.req.get_headers()["X-Real-IP"] ngx.header["Base-Header"] = ngx.req.get_headers()["Base-Header"] ngx.header["Base-Query"] = ngx.var.arg_base ngx.header["X-Res"] = "C" diff --git a/t/plugin/batch-requests2.t b/t/plugin/batch-requests2.t index 046dfe335c4f..ba578c47c9a4 100644 --- a/t/plugin/batch-requests2.t +++ b/t/plugin/batch-requests2.t @@ -30,6 +30,13 @@ add_block_preprocessor(sub { if (!$block->no_error_log && !$block->error_log) { $block->set_value("no_error_log", "[error]\n[alert]"); } + + my $extra_yaml_config = <<_EOC_; +plugins: + - batch-requests +_EOC_ + + $block->set_value("extra_yaml_config", $extra_yaml_config); }); run_tests; From b358db4a1ad2872d56728ce7ef8a8579103dc159 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 12 Nov 2021 10:41:12 +0800 Subject: [PATCH 089/260] docs: fix borken link in `FAQ.md` (#5490) Signed-off-by: leslie --- docs/en/latest/FAQ.md | 2 +- docs/zh/latest/FAQ.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md index 6dd2d6287467..8038b9c14685 100644 --- a/docs/en/latest/FAQ.md +++ b/docs/en/latest/FAQ.md @@ -59,7 +59,7 @@ For the configuration center, configuration storage is only the most basic funct 4. Change Notification 5. High Performance -See more [etcd why](https://github.com/etcd-io/website/blob/master/content/en/docs/next/learning/why.md#comparison-chart). +See more [etcd why](https://etcd.io/docs/latest/learning/why/#comparison-chart). ## Why is it that installing Apache APISIX dependencies with Luarocks causes timeout, slow or unsuccessful installation? diff --git a/docs/zh/latest/FAQ.md b/docs/zh/latest/FAQ.md index 5808f745f352..742a215a3a55 100644 --- a/docs/zh/latest/FAQ.md +++ b/docs/zh/latest/FAQ.md @@ -58,7 +58,7 @@ APISIX 是当前性能最好的 API 网关,单核 QPS 达到 2.3 万,平均 4. 变化通知 5. 高性能 -APISIX 需要一个配置中心,上面提到的很多功能是传统关系型数据库和 KV 数据库是无法提供的。与 etcd 同类软件还有 Consul、ZooKeeper 等,更详细比较可以参考这里:[etcd why](https://github.com/etcd-io/website/blob/master/content/en/docs/next/learning/why.md#comparison-chart),在将来也许会支持其他配置存储方案。 +APISIX 需要一个配置中心,上面提到的很多功能是传统关系型数据库和 KV 数据库是无法提供的。与 etcd 同类软件还有 Consul、ZooKeeper 等,更详细比较可以参考这里:[etcd why](https://etcd.io/docs/latest/learning/why/#comparison-chart),在将来也许会支持其他配置存储方案。 ## 为什么在用 Luarocks 安装 APISIX 依赖时会遇到超时,很慢或者不成功的情况? From 4a57523627d9b3bbd96b1b2c20aa6689c0cc1877 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Fri, 12 Nov 2021 08:11:37 +0530 Subject: [PATCH 090/260] docs: custom configuration for datadog plugin (#5486) --- docs/en/latest/plugins/datadog.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/en/latest/plugins/datadog.md b/docs/en/latest/plugins/datadog.md index 7842018eb182..f99e9b5b6114 100644 --- a/docs/en/latest/plugins/datadog.md +++ b/docs/en/latest/plugins/datadog.md @@ -30,6 +30,7 @@ title: datadog - [Exported Metrics](#exported-metrics) - [How To Enable](#how-to-enable) - [Disable Plugin](#disable-plugin) +- [Custom Configuration](#custom-configuration) ## Name @@ -129,3 +130,30 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f } }' ``` + +## Custom Configuration + +In the default configuration, the plugin expects the dogstatsd service to be available at `127.0.0.1:8125`. If you wish to update the config, please update the plugin metadata. To know more about the fields of the datadog metadata, see [here](#metadata). + +Make a request to _/apisix/admin/plugin_metadata_ endpoint with the updated metadata as following: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/datadog -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "host": "172.168.45.29", + "port": 8126, + "constant_tags": [ + "source:apisix", + "service:custom" + ], + "namespace": "apisix" +}' +``` + +This HTTP PUT request will update the metadata and subsequent metrics will be pushed to the `172.168.45.29:8126` endpoint via UDP StatsD. Everything will be hot-loaded, there is no need to restart Apache APISIX instances. + +In case, if you wish to revert the datadog metadata schema to the default values, just make another PUT request to the same endpoint with an empty body. For example: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/datadog -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '{}' +``` From 0570d59fd0848a72a3341634ed7553476b0213f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 12 Nov 2021 10:42:14 +0800 Subject: [PATCH 091/260] docs: remove duplicate Dubbo entry (#5483) Signed-off-by: spacewander --- README.md | 1 - docs/zh/latest/README.md | 1 - 2 files changed, 2 deletions(-) diff --git a/README.md b/README.md index 2e4c114c3435..fea4087812ff 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - [gRPC transcoding](docs/en/latest/plugins/grpc-transcode.md): Supports protocol transcoding so that clients can access your gRPC API by using HTTP/JSON. - Proxy Websocket - Proxy Protocol - - Proxy Dubbo: Dubbo Proxy based on Tengine. - HTTP(S) Forward Proxy - [SSL](docs/en/latest/certificate.md): Dynamically load an SSL certificate. diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index c8ae97bf9fab..9164d0f4d576 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -78,7 +78,6 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - [gRPC 协议转换](plugins/grpc-transcode.md):支持协议的转换,这样客户端可以通过 HTTP/JSON 来访问你的 gRPC API。 - Websocket 代理 - Proxy Protocol - - Dubbo 代理:基于 Tengine,可以实现 Dubbo 请求的代理。 - HTTP(S) 反向代理 - [SSL](certificate.md):动态加载 SSL 证书。 From 5f6afdbc3a0872ce6559449bc7917e48e9343656 Mon Sep 17 00:00:00 2001 From: Daming Date: Fri, 12 Nov 2021 15:17:27 +0800 Subject: [PATCH 092/260] feat: provide skywalking logger plugin (#5478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 Co-authored-by: 吴晟 Wu Sheng --- apisix/plugins/skywalking-logger.lua | 233 +++++++++++++++++ conf/config-default.yaml | 1 + docs/en/latest/config.json | 1 + docs/en/latest/plugins/skywalking-logger.md | 117 +++++++++ docs/zh/latest/config.json | 1 + docs/zh/latest/plugins/skywalking-logger.md | 115 +++++++++ t/admin/plugins.t | 2 +- t/plugin/skywalking-logger.t | 269 ++++++++++++++++++++ 8 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 apisix/plugins/skywalking-logger.lua create mode 100644 docs/en/latest/plugins/skywalking-logger.md create mode 100644 docs/zh/latest/plugins/skywalking-logger.md create mode 100644 t/plugin/skywalking-logger.t diff --git a/apisix/plugins/skywalking-logger.lua b/apisix/plugins/skywalking-logger.lua new file mode 100644 index 000000000000..58f25b84b0f7 --- /dev/null +++ b/apisix/plugins/skywalking-logger.lua @@ -0,0 +1,233 @@ +-- +-- 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 batch_processor = require("apisix.utils.batch-processor") +local log_util = require("apisix.utils.log-util") +local core = require("apisix.core") +local http = require("resty.http") +local url = require("net.url") +local plugin = require("apisix.plugin") + +local base64 = require("ngx.base64") +local ngx_re = require("ngx.re") + +local ngx = ngx +local tostring = tostring +local tonumber = tonumber +local ipairs = ipairs +local timer_at = ngx.timer.at + +local plugin_name = "skywalking-logger" +local stale_timer_running = false +local buffers = {} + +local schema = { + type = "object", + properties = { + endpoint_addr = core.schema.uri_def, + service_name = {type = "string", default = "APISIX"}, + service_instance_name = {type = "string", default = "APISIX Instance Name"}, + timeout = {type = "integer", minimum = 1, default = 3}, + name = {type = "string", default = "skywalking logger"}, + max_retry_count = {type = "integer", minimum = 0, default = 0}, + retry_delay = {type = "integer", minimum = 0, default = 1}, + buffer_duration = {type = "integer", minimum = 1, default = 60}, + inactive_timeout = {type = "integer", minimum = 1, default = 5}, + batch_max_size = {type = "integer", minimum = 1, default = 1000}, + include_req_body = {type = "boolean", default = false}, + }, + required = {"endpoint_addr"}, +} + + +local metadata_schema = { + type = "object", + properties = { + log_format = log_util.metadata_schema_log_format, + }, +} + + +local _M = { + version = 0.1, + priority = 408, + name = plugin_name, + schema = schema, + metadata_schema = metadata_schema, +} + + +function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + end + return core.schema.check(schema, conf) +end + + +local function send_http_data(conf, log_message) + local err_msg + local res = true + local url_decoded = url.parse(conf.endpoint_addr) + local host = url_decoded.host + local port = url_decoded.port + + core.log.info("sending a batch logs to ", conf.endpoint_addr) + + local httpc = http.new() + httpc:set_timeout(conf.timeout * 1000) + local ok, err = httpc:connect(host, port) + + if not ok then + return false, "failed to connect to host[" .. host .. "] port[" + .. tostring(port) .. "] " .. err + end + + local httpc_res, httpc_err = httpc:request({ + method = "POST", + path = "/v3/logs", + body = log_message, + headers = { + ["Host"] = url_decoded.host, + ["Content-Type"] = "application/json", + } + }) + + if not httpc_res then + return false, "error while sending data to [" .. host .. "] port[" + .. tostring(port) .. "] " .. httpc_err + end + + -- some error occurred in the server + if httpc_res.status >= 400 then + res = false + err_msg = "server returned status code[" .. httpc_res.status .. "] host[" + .. host .. "] port[" .. tostring(port) .. "] " + .. "body[" .. httpc_res:read_body() .. "]" + end + + return res, err_msg +end + + +-- remove stale objects from the memory after timer expires +local function remove_stale_objects(premature) + if premature then + return + end + + for key, batch in ipairs(buffers) do + if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then + core.log.warn("removing batch processor stale object, conf: ", + core.json.delay_encode(key)) + buffers[key] = nil + end + end + + stale_timer_running = false +end + + +function _M.log(conf, ctx) + local metadata = plugin.plugin_metadata(plugin_name) + core.log.info("metadata: ", core.json.delay_encode(metadata)) + + local log_body + if metadata and metadata.value.log_format + and core.table.nkeys(metadata.value.log_format) > 0 + then + log_body = log_util.get_custom_format_log(ctx, metadata.value.log_format) + else + log_body = log_util.get_full_log(ngx, conf) + end + + local trace_context + local sw_header = ngx.req.get_headers()["sw8"] + if sw_header then + -- 1-TRACEID-SEGMENTID-SPANID-PARENT_SERVICE-PARENT_INSTANCE-PARENT_ENDPOINT-IPPORT + local ids = ngx_re.split(sw_header, '-') + if #ids == 8 then + trace_context = { + traceId = base64.decode_base64url(ids[2]), + traceSegmentId = base64.decode_base64url(ids[3]), + spanId = tonumber(ids[4]) + } + else + core.log.warn("failed to parse trace_context header: ", sw_header) + end + end + + local entry = { + traceContext = trace_context, + body = { + json = { + json = core.json.encode(log_body, true) + } + }, + service = conf.service_name, + serviceInstance = conf.service_instance_name, + endpoint = ctx.var.uri, + } + + if not stale_timer_running then + -- run the timer every 30 mins if any log is present + timer_at(1800, remove_stale_objects) + stale_timer_running = true + end + + local log_buffer = buffers[conf] + + if log_buffer then + log_buffer:push(entry) + return + end + + -- Generate a function to be executed by the batch processor + local func = function(entries, batch_max_size) + local data, err = core.json.encode(entries) + if not data then + return false, 'error occurred while encoding the data: ' .. err + end + + return send_http_data(conf, data) + end + + local config = { + name = conf.name, + retry_delay = conf.retry_delay, + batch_max_size = conf.batch_max_size, + max_retry_count = conf.max_retry_count, + buffer_duration = conf.buffer_duration, + inactive_timeout = conf.inactive_timeout, + route_id = ctx.var.route_id, + server_addr = ctx.var.server_addr, + } + + local err + log_buffer, err = batch_processor:new(func, config) + + if not log_buffer then + core.log.error("error when creating the batch processor: ", err) + return + end + + buffers[conf] = log_buffer + log_buffer:push(entry) +end + + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 922a91fc8e0d..e0ea36f22575 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -344,6 +344,7 @@ plugins: # plugin list (sorted by priority) - datadog # priority: 495 - echo # priority: 412 - http-logger # priority: 410 + - skywalking-logger # priority: 408 - sls-logger # priority: 406 - tcp-logger # priority: 405 - kafka-logger # priority: 403 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 89236d3a17b9..a9fcbb498f1c 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -113,6 +113,7 @@ "label": "Loggers", "items": [ "plugins/http-logger", + "plugins/skywalking-logger", "plugins/tcp-logger", "plugins/kafka-logger", "plugins/udp-logger", diff --git a/docs/en/latest/plugins/skywalking-logger.md b/docs/en/latest/plugins/skywalking-logger.md new file mode 100644 index 000000000000..50f365910eab --- /dev/null +++ b/docs/en/latest/plugins/skywalking-logger.md @@ -0,0 +1,117 @@ +--- +title: skywalking-logger +--- + + + +## Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Metadata**](#metadata) +- [**Disable Plugin**](#disable-plugin) + +## Name + +`skywalking-logger` is a plugin which push Access Log data to `SkyWalking OAP` server over HTTP. If there is tracing context existing, it sets up the trace-log correlation automatically, and relies on [SkyWalking Cross Process Propagation Headers Protocol](https://skywalking.apache.org/docs/main/latest/en/protocols/skywalking-cross-process-propagation-headers-protocol-v3/). + +This will provide the ability to send Access Log as JSON objects to `SkyWalking OAP` server. + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| ---------------- | ------- | ----------- | ------------- | ------- | ---------------------------------------------------------------------------------------- | +| endpoint_addr | string | required | | | The URI of the `SkyWalking OAP` server. | +| service_name | string | optional | "APISIX" | | service name for SkyWalking reporter. | +| service_instance_name | string | optional |"APISIX Instance Name" | | service instance name for SkyWalking reporter, set it to `$hostname` to get local hostname directly.| +| timeout | integer | optional | 3 | [1,...] | Time to keep the connection alive after sending a request. | +| name | string | optional | "skywalking logger" | | A unique identifier to identity the logger. | +| batch_max_size | integer | optional | 1000 | [1,...] | Set the maximum number of logs sent in each batch. When the number of logs reaches the set maximum, all logs will be automatically pushed to the `SkyWalking OAP` server. | +| inactive_timeout | integer | optional | 5 | [1,...] | The maximum time to refresh the buffer (in seconds). When the maximum refresh time is reached, all logs will be automatically pushed to the `SkyWalking OAP` server regardless of whether the number of logs in the buffer reaches the maximum number set. | +| buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed.| +| max_retry_count | integer | optional | 0 | [0,...] | Maximum number of retries before removing from the processing pipe line. | +| retry_delay | integer | optional | 1 | [0,...] | Number of seconds the process execution should be delayed if the execution fails. | +| include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. | + +## How To Enable + +The following is an example of how to enable the `skywalking-logger` for a specific route. Before that, an available `SkyWalking OAP` server was required and accessible. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "http-logger": { + "endpoint_addr": "http://127.0.0.1:12800" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +## Test Plugin + +> success: + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +... +hello, world +``` + +Completion of the steps, could find the Log details on `SkyWalking UI`. + +## Metadata + +`skywalking-logger` also supports to custom log format like [http-logger](./http-logger.md). + +| Name | Type | Requirement | Default | Valid | Description | +| ---------------- | ------- | ----------- | ------------- | ------- | ---------------------------------------------------------------------------------------- | +| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get `APISIX` variables or [Nginx variable](http://nginx.org/en/docs/varindex.html). | + + Note that **the metadata configuration is applied in global scope**, which means it will take effect on all Route or Service which use `skywalking-logger` plugin. + +## Disable Plugin + +Remove the corresponding json configuration in the plugin configuration to disable the `skywalking-logger`. +APISIX plugins are hot-reloaded, therefore no need to restart APISIX. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index f2f863d0ffc7..d8ddedc55a0f 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -111,6 +111,7 @@ "label": "Loggers", "items": [ "plugins/http-logger", + "plugins/skywalking-logger", "plugins/tcp-logger", "plugins/kafka-logger", "plugins/udp-logger", diff --git a/docs/zh/latest/plugins/skywalking-logger.md b/docs/zh/latest/plugins/skywalking-logger.md new file mode 100644 index 000000000000..3476c75fdd1b --- /dev/null +++ b/docs/zh/latest/plugins/skywalking-logger.md @@ -0,0 +1,115 @@ +--- +title: skywalking-logger +--- + + + +## 目录 + +- [**定义**](#定义) +- [**属性列表**](#属性列表) +- [**如何开启**](#如何开启) +- [**测试插件**](#测试插件) +- [**插件元数据设置**](#插件元数据设置) +- [**禁用插件**](#禁用插件) + +## 定义 + +`http-logger` 是一个插件,可将Access Log数据通过`HTTP`推送到 SkyWalking OAP 服务器。如果上下文中存在`tracing context`,插件会自动建立`trace`与日志的关联,并依赖于 [SkyWalking Cross Process Propagation Headers Protocol](https://skywalking.apache.org/docs/main/latest/en/protocols/skywalking-cross-process-propagation-headers-protocol-v3/) 。 + +这将提供将 Access Log 数据作为JSON对象发送到 SkyWalking OAP 服务器的功能。 + +## 属性列表 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | +| uri | string | 必须 | | | `SkyWalking OAp` 服务器的 URI。 | +| timeout | integer | 可选 | 3 | [1,...] | 发送请求后保持连接活动的时间。 | +| name | string | 可选 | "skywalking logger" | | 标识 logger 的唯一标识符。 | +| batch_max_size | integer | 可选 | 1000 | [1,...] | 设置每批发送日志的最大条数,当日志条数达到设置的最大值时,会自动推送全部日志到 `HTTP/HTTPS` 服务。 | +| inactive_timeout | integer | 可选 | 5 | [1,...] | 刷新缓冲区的最大时间(以秒为单位),当达到最大的刷新时间时,无论缓冲区中的日志数量是否达到设置的最大条数,也会自动将全部日志推送到 `HTTP/HTTPS` 服务。 | +| buffer_duration | integer | 可选 | 60 | [1,...] | 必须先处理批次中最旧条目的最长期限(以秒为单位)。 | +| max_retry_count | integer | 可选 | 0 | [0,...] | 从处理管道中移除之前的最大重试次数。 | +| retry_delay | integer | 可选 | 1 | [0,...] | 如果执行失败,则应延迟执行流程的秒数。 | +| include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ; true: 表示包含请求的 body 。 | + +## 如何开启 + +这是有关如何为特定路由启用 `skywalking-logger` 插件的示例。在此之前,需要有可用的 SkyWalking OAP 可以被访问。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "skywalking-logger": { + "uri": "http://127.0.0.1:12800" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +## 测试插件 + +> 成功: + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +... +hello, world +``` + +完成上述步骤后,在可以 SkyWalking UI 查看到相关日志。 + +## 插件元数据设置 + +`skywalking-logger` 也是制定日志格式,与 [http-logger](./http-logger.md) 插件类似。 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | +| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 __APISIX__ 变量或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。| + +特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 skywalking-logger 的 Route 或 Service 生效。 + +## 禁用插件 + +在插件配置中删除相应的 json 配置以禁用 skywalking-logger。APISIX 插件是热重载的,因此无需重新启动 APISIX: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index cc95a649450a..8e69ef8a294b 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -40,7 +40,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ +qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ --- no_error_log [error] diff --git a/t/plugin/skywalking-logger.t b/t/plugin/skywalking-logger.t new file mode 100644 index 000000000000..2c9c3143b4da --- /dev/null +++ b/t/plugin/skywalking-logger.t @@ -0,0 +1,269 @@ +# +# 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'; + +log_level('debug'); +repeat_each(1); +no_long_string(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $http_config = $block->http_config // <<_EOC_; + + server { + listen 1986; + server_tokens off; + + location /v3/logs { + content_by_lua_block { + local core = require("apisix.core") + + core.log.warn(core.json.encode(core.request.get_body(), true)) + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.skywalking-logger") + local ok, err = plugin.check_schema({endpoint_addr = "http://127.0.0.1"}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } + + + +=== TEST 2: full schema check +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.skywalking-logger") + local ok, err = plugin.check_schema({endpoint_addr = "http://127.0.0.1", + timeout = 3, + name = "skywalking-logger", + max_retry_count = 2, + retry_delay = 2, + buffer_duration = 2, + inactive_timeout = 2, + batch_max_size = 500, + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } + + + +=== TEST 3: uri is missing +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.skywalking-logger") + local ok, err = plugin.check_schema({timeout = 3, + name = "skywalking-logger", + max_retry_count = 2, + retry_delay = 2, + buffer_duration = 2, + inactive_timeout = 2, + batch_max_size = 500, + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +property "endpoint_addr" is required +done + + + +=== TEST 4: add plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "skywalking-logger": { + "endpoint_addr": "http://127.0.0.1:1986", + "batch_max_size": 1, + "max_retry_count": 1, + "retry_delay": 2, + "buffer_duration": 2, + "inactive_timeout": 2 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]], + [[{ + "node": { + "value": { + "plugins": { + "skywalking-logger": { + "endpoint_addr": "http://127.0.0.1:1986", + "batch_max_size": 1, + "max_retry_count": 1, + "retry_delay": 2, + "buffer_duration": 2, + "inactive_timeout": 2 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: access local server +--- request +GET /opentracing +--- response_body +opentracing +--- error_log +Batch Processor[skywalking logger] successfully processed the entries +--- wait: 0.5 + + + +=== TEST 6: test trace context header +--- request +GET /opentracing +--- more_headers +sw8: 1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-1-QVBJU0lY-QVBJU0lYIEluc3RhbmNlIE5hbWU=-L2dldA==-dXBzdHJlYW0gc2VydmljZQ== +--- response_body +opentracing +--- error_log eval +qr/.*\\\"traceContext\\\":\{(\\\"traceSegmentId\\\":\\\"ae709769-6e20-4c68-9733-10f455216cc5\\\"|\\\"traceId\\\":\\\"ae709769-6e20-4c68-9733-10f455216cc5\\\"|\\\"spanId\\\":1|,){5}\}.*/ +--- wait: 0.5 + + + +=== TEST 7: test wrong trace context header +--- request +GET /opentracing +--- more_headers +sw8: 1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-YWU3MDk3NjktNmUyMC00YzY4LTk3MzMtMTBmNDU1MjE2Y2M1-1-QVBJU0lY-QVBJU0lYIEluc3RhbmNlIE5hbWU=-L2dldA== +--- response_body +opentracing +--- error_log eval +qr/failed to parse trace_context header:/ +--- wait: 0.5 + + + +=== TEST 8: add plugin metadata +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/skywalking-logger', + ngx.HTTP_PUT, + [[{ + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } + }]], + [[{ + "node": { + "value": { + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: access local server and test log format +--- request +GET /opentracing +--- response_body +opentracing +--- error_log eval +qr/.*\{\\\"json\\\":\\\"\{(\\\\\\\"\@timestamp\\\\\\\":\\\\\\\".*\\\\\\\"|\\\\\\\"client_ip\\\\\\\":\\\\\\\"127\.0\.0\.1\\\\\\\"|\\\\\\\"host\\\\\\\":\\\\\\\"localhost\\\\\\\"|\\\\\\\"route_id\\\\\\\":\\\\\\\"1\\\\\\\"|,){7}\}/ +--- wait: 0.5 From 4cd1edcc39a1733c0ea10e2eccc324db47a112c8 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Fri, 12 Nov 2021 12:57:27 +0530 Subject: [PATCH 093/260] refactor: Method for latency calculation and related refactoring (#5480) --- apisix/plugins/datadog.lua | 12 ++++-------- apisix/plugins/prometheus/exporter.lua | 17 ++++------------- apisix/utils/log-util.lua | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/apisix/plugins/datadog.lua b/apisix/plugins/datadog.lua index 0a6a134bcff2..6a55886f3a9c 100644 --- a/apisix/plugins/datadog.lua +++ b/apisix/plugins/datadog.lua @@ -18,6 +18,7 @@ local core = require("apisix.core") local plugin = require("apisix.plugin") local batch_processor = require("apisix.utils.batch-processor") local fetch_log = require("apisix.utils.log-util").get_full_log +local latency_details = require("apisix.utils.log-util").latency_details_in_ms local service_fetch = require("apisix.http.service").get local ngx = ngx local udp = ngx.socket.udp @@ -139,7 +140,7 @@ function _M.log(conf, ctx) end local entry = fetch_log(ngx, {}) - entry.upstream_latency = ctx.var.upstream_response_time * 1000 + entry.latency, entry.upstream_latency, entry.apisix_latency = latency_details(ctx) entry.balancer_ip = ctx.balancer_ip or "" entry.scheme = ctx.upstream_scheme or "" @@ -215,23 +216,18 @@ function _M.log(conf, ctx) end -- upstream latency - local apisix_latency = entry.latency if entry.upstream_latency then local ok, err = sock:send(format("%s:%s|%s%s", prefix .. - "upstream.latency", entry.upstream_latency, "h", suffix)) + "upstream.latency", entry.upstream_latency, "h", suffix)) if not ok then core.log.error("failed to report upstream latency to dogstatsd server: host[" .. host .. "] port[" .. tostring(port) .. "] err: " .. err) end - apisix_latency = apisix_latency - entry.upstream_latency - if apisix_latency < 0 then - apisix_latency = 0 - end end -- apisix_latency local ok, err = sock:send(format("%s:%s|%s%s", prefix .. - "apisix.latency", apisix_latency, "h", suffix)) + "apisix.latency", entry.apisix_latency, "h", suffix)) if not ok then core.log.error("failed to report apisix latency to dogstatsd server: host[" .. host .. "] port[" .. tostring(port) .. "] err: " .. err) diff --git a/apisix/plugins/prometheus/exporter.lua b/apisix/plugins/prometheus/exporter.lua index b2c1ebcd952a..d565b4c40bd4 100644 --- a/apisix/plugins/prometheus/exporter.lua +++ b/apisix/plugins/prometheus/exporter.lua @@ -33,6 +33,7 @@ local clear_tab = core.table.clear local get_stream_routes = router.stream_routes local get_protos = require("apisix.plugins.grpc-transcode.proto").protos local service_fetch = require("apisix.http.service").get +local latency_details = require("apisix.utils.log-util").latency_details_in_ms @@ -143,25 +144,15 @@ function _M.log(conf, ctx) gen_arr(vars.status, route_id, matched_uri, matched_host, service_id, consumer_name, balancer_ip)) - local latency = (ngx.now() - ngx.req.start_time()) * 1000 + local latency, upstream_latency, apisix_latency = latency_details(ctx) metrics.latency:observe(latency, gen_arr("request", route_id, service_id, consumer_name, balancer_ip)) - local apisix_latency = latency - if ctx.var.upstream_response_time then - local upstream_latency = ctx.var.upstream_response_time * 1000 + if upstream_latency then metrics.latency:observe(upstream_latency, gen_arr("upstream", route_id, service_id, consumer_name, balancer_ip)) - apisix_latency = apisix_latency - upstream_latency - - -- The latency might be negative, as Nginx uses different time measurements in - -- different metrics. - -- See https://github.com/apache/apisix/issues/5146#issuecomment-928919399 - if apisix_latency < 0 then - apisix_latency = 0 - end - end + metrics.latency:observe(apisix_latency, gen_arr("apisix", route_id, service_id, consumer_name, balancer_ip)) diff --git a/apisix/utils/log-util.lua b/apisix/utils/log-util.lua index cf3fc22f1c3b..10620d1b7387 100644 --- a/apisix/utils/log-util.lua +++ b/apisix/utils/log-util.lua @@ -153,4 +153,23 @@ function _M.get_req_original(ctx, conf) end +function _M.latency_details_in_ms(ctx) + local latency = (ngx.now() - ngx.req.start_time()) * 1000 + local upstream_latency, apisix_latency = nil, latency + + if ctx.var.upstream_response_time then + upstream_latency = ctx.var.upstream_response_time * 1000 + apisix_latency = apisix_latency - upstream_latency + + -- The latency might be negative, as Nginx uses different time measurements in + -- different metrics. + -- See https://github.com/apache/apisix/issues/5146#issuecomment-928919399 + if apisix_latency < 0 then + apisix_latency = 0 + end + end + + return latency, upstream_latency, apisix_latency +end + return _M From 8206f475639d6de633ff4ea50a8e1c2ce9b36fac Mon Sep 17 00:00:00 2001 From: iGeeky Date: Fri, 12 Nov 2021 15:49:55 +0800 Subject: [PATCH 094/260] change(wolf-rbac): change default port number and add `authType` parameter to documentation (#5477) --- apisix/plugins/wolf-rbac.lua | 2 +- docs/en/latest/plugins/wolf-rbac.md | 7 ++++--- docs/zh/latest/plugins/wolf-rbac.md | 7 ++++--- t/plugin/wolf-rbac.t | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apisix/plugins/wolf-rbac.lua b/apisix/plugins/wolf-rbac.lua index 1756004bc98e..92344545fce3 100644 --- a/apisix/plugins/wolf-rbac.lua +++ b/apisix/plugins/wolf-rbac.lua @@ -49,7 +49,7 @@ local schema = { }, server = { type = "string", - default = "http://127.0.0.1:10080" + default = "http://127.0.0.1:12180" }, header_prefix = { type = "string", diff --git a/docs/en/latest/plugins/wolf-rbac.md b/docs/en/latest/plugins/wolf-rbac.md index c76310e85fac..96ced3a3503a 100644 --- a/docs/en/latest/plugins/wolf-rbac.md +++ b/docs/en/latest/plugins/wolf-rbac.md @@ -39,7 +39,7 @@ The rbac feature is provided by [wolf](https://github.com/iGeeky/wolf). For more | Name | Type | Requirement | Default | Valid | Description | | ------------- | ------ | ----------- | ------------------------ | ----- | --------------------------------------------------------- | -| server | string | optional | "http://127.0.0.1:10080" | | Set the service address of `wolf-server`. | +| server | string | optional | "http://127.0.0.1:12180" | | Set the service address of `wolf-server`. | | appid | string | optional | "unset" | | Set the app id. The app id must be added in wolf-console. | | header_prefix | string | optional | "X-" | | prefix of custom HTTP header. After authentication is successful, three headers will be added to the request header (for backend) and response header (for frontend): `X-UserId`, `X-Username`, `X-Nickname`. | @@ -73,7 +73,7 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f "username":"wolf_rbac", "plugins":{ "wolf-rbac":{ - "server":"http://127.0.0.1:10080", + "server":"http://127.0.0.1:12180", "appid":"restful" } }, @@ -113,13 +113,14 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 #### Login and get `wolf-rbac` token: The following `appid`, `username`, and `password` must be real ones in the wolf system. +`authType` is the authentication type, `1` is password authentication, `2` is `LDAP` authentication. The default is `1`. `wolf` supports `LDAP` authentication since version 0.5.0 * Login as `POST application/json` ```shell curl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \ -H "Content-Type: application/json" \ --d '{"appid": "restful", "username":"test", "password":"user-password"}' +-d '{"appid": "restful", "username":"test", "password":"user-password", "authType":1}' HTTP/1.1 200 OK Date: Wed, 24 Jul 2019 10:33:31 GMT diff --git a/docs/zh/latest/plugins/wolf-rbac.md b/docs/zh/latest/plugins/wolf-rbac.md index a176e0fea1ba..153d776e5d15 100644 --- a/docs/zh/latest/plugins/wolf-rbac.md +++ b/docs/zh/latest/plugins/wolf-rbac.md @@ -39,7 +39,7 @@ rbac 功能由[wolf](https://github.com/iGeeky/wolf)提供, 有关 `wolf` 的更 | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ------------- | ------ | ------ | ------------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| server | string | 可选 | "http://127.0.0.1:10080" | | 设置 `wolf-server` 的访问地址 | +| server | string | 可选 | "http://127.0.0.1:12180" | | 设置 `wolf-server` 的访问地址 | | appid | string | 可选 | "unset" | | 设置应用 id, 该应用 id, 需要是在 `wolf-console` 中已经添加的应用 id | | header_prefix | string | 可选 | "X-" | | 自定义 http 头的前缀。`wolf-rbac`在鉴权成功后, 会在请求头(用于传给后端)及响应头(用于传给前端)中添加 3 个头: `X-UserId`, `X-Username`, `X-Nickname` | @@ -73,7 +73,7 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f "username":"wolf_rbac", "plugins":{ "wolf-rbac":{ - "server":"http://127.0.0.1:10080", + "server":"http://127.0.0.1:12180", "appid":"restful" } }, @@ -113,13 +113,14 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 #### 首先进行登录获取 `wolf-rbac` token: 下面的 `appid`, `username`, `password` 必须为 wolf 系统中真实存在的. +`authType` 为认证类型, `1` 为密码认证, `2` 为 `LDAP` 认证. 默认为 `1`. `wolf` 从 0.5.0 版本开始支持了 `LDAP` 认证. * 以 POST application/json 方式登陆. ```shell curl http://127.0.0.1:9080/apisix/plugin/wolf-rbac/login -i \ -H "Content-Type: application/json" \ --d '{"appid": "restful", "username":"test", "password":"user-password"}' +-d '{"appid": "restful", "username":"test", "password":"user-password", "authType":1}' HTTP/1.1 200 OK Date: Wed, 24 Jul 2019 10:33:31 GMT diff --git a/t/plugin/wolf-rbac.t b/t/plugin/wolf-rbac.t index 530a9c476949..dc2ae7c894c6 100644 --- a/t/plugin/wolf-rbac.t +++ b/t/plugin/wolf-rbac.t @@ -45,7 +45,7 @@ __DATA__ --- request GET /t --- response_body_like eval -qr/\{"appid":"unset","header_prefix":"X-","server":"http:\/\/127\.0\.0\.1:10080"\}/ +qr/\{"appid":"unset","header_prefix":"X-","server":"http:\/\/127\.0\.0\.1:12180"\}/ --- no_error_log [error] From 6c5108ff3896b96e1bf0a0a68fd2e7a9aadc1162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 14 Nov 2021 15:08:55 +0800 Subject: [PATCH 095/260] fix: response.set_header can remove header like request.set_header (#5499) Signed-off-by: spacewander --- apisix/core/response.lua | 4 +++- t/core/response.t | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apisix/core/response.lua b/apisix/core/response.lua index e1f0f4d753a6..9ce9c400daad 100644 --- a/apisix/core/response.lua +++ b/apisix/core/response.lua @@ -101,7 +101,9 @@ local function set_header(append, ...) if count == 1 then local headers = select(1, ...) if type(headers) ~= "table" then - error("should be a table if only one argument", 2) + -- response.set_header(name, nil) + ngx_header[headers] = nil + return end for k, v in pairs(headers) do diff --git a/t/core/response.t b/t/core/response.t index eb8eda8b28e4..ed7856be2a9d 100644 --- a/t/core/response.t +++ b/t/core/response.t @@ -142,3 +142,24 @@ aaa: bbb, bbb ccc: ddd --- no_error_log [error] + + + +=== TEST 7: delete header +--- config + location = /t { + access_by_lua_block { + local core = require("apisix.core") + core.response.set_header("aaa", "bbb") + core.response.set_header("aaa", nil) + core.response.exit(200, "done\n") + } + } +--- request +GET /t +--- response_body +done +--- response_headers +aaa: +--- no_error_log +[error] From 12af57fc726dd1791f5aecedf25d37f4a1d74110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 14 Nov 2021 19:25:36 +0800 Subject: [PATCH 096/260] ci: rpm is published by build tools now (#5498) --- .github/workflows/centos7-ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 00f1125e25a9..dd8cb40c76b7 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -80,10 +80,3 @@ jobs: - name: Run test cases run: | docker exec centos7Instance bash -c "cd apisix && ./ci/centos7-ci.sh run_case" - - - name: Publish Artifact - if: ${{ startsWith(github.ref, 'refs/heads/release/') }} - uses: actions/upload-artifact@v2.2.4 - with: - name: "rpm" - path: "./apisix-build-tools/output/apisix-${{ steps.branch_env.outputs.version }}-0.el7.x86_64.rpm" From d13e7f7f0b3f6001cb634598e533a23658927285 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 15 Nov 2021 00:07:34 -0600 Subject: [PATCH 097/260] feat: release 2.10.2 (#5508) --- CHANGELOG.md | 21 +++++++ apisix/core/version.lua | 2 +- docs/en/latest/config.json | 2 +- docs/en/latest/how-to-build.md | 14 ++--- docs/zh/latest/CHANGELOG.md | 21 +++++++ docs/zh/latest/config.json | 2 +- docs/zh/latest/how-to-build.md | 14 ++--- rockspec/apisix-2.10.2-0.rockspec | 95 +++++++++++++++++++++++++++++++ 8 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 rockspec/apisix-2.10.2-0.rockspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 790a4b19e437..fdcfb887e4a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ title: Changelog ## Table of Contents +- [2.10.2](#2102) - [2.10.1](#2101) - [2.10.0](#2100) - [2.9.0](#290) @@ -47,6 +48,26 @@ title: Changelog - [0.7.0](#070) - [0.6.0](#060) +## 2.10.2 + +### Bugfix + +- fix: response.set_header should remove header like request.set_header [#5499](https://github.com/apache/apisix/pull/5499) +- fix(batch-requests): correct the client ip in the pipeline [#5476](https://github.com/apache/apisix/pull/5476) +- fix(upstream): load imbalance when it's referred by multiple routes [#5462](https://github.com/apache/apisix/pull/5462) +- fix(hmac-auth): check if the X-HMAC-ALGORITHM header is missing [#5467](https://github.com/apache/apisix/pull/5467) +- fix: prevent being hacked by untrusted request_uri [#5458](https://github.com/apache/apisix/pull/5458) +- fix(admin): modify boolean parameters with PATCH [#5434](https://github.com/apache/apisix/pull/5432) +- fix(auth-ldap): add handler for invalid basic auth header values [#5432](https://github.com/apache/apisix/pull/5432) +- fix(traffic-split): multiple rules with multiple weighted_upstreams under each rule cause upstream_key duplicate [#5414](https://github.com/apache/apisix/pull/5414) +- fix: add handler for invalid basic auth header values [#5419](https://github.com/apache/apisix/pull/5419) +- fix: ldap deps in install-dependencies.sh [#5385](https://github.com/apache/apisix/pull/5385) +- fix(request-validation): correct rejected_message to rejected_msg [#5355](https://github.com/apache/apisix/pull/5355) + +### Change + +- change: log insensitive consumer info only [#5445](https://github.com/apache/apisix/pull/5445) + ## 2.10.1 ### Bugfix diff --git a/apisix/core/version.lua b/apisix/core/version.lua index 350f101bf90b..1761fb51937a 100644 --- a/apisix/core/version.lua +++ b/apisix/core/version.lua @@ -15,5 +15,5 @@ -- limitations under the License. -- return { - VERSION = "2.10.1" + VERSION = "2.10.2" } diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index a9fcbb498f1c..cb45e103c3e3 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -1,5 +1,5 @@ { - "version": "2.10.1", + "version": "2.10.2", "sidebar": [ { "type": "category", diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 98b00bcd3ff3..ff8ea580198b 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -58,7 +58,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep This installation method is suitable for CentOS 7, please run the following command to install Apache APISIX. ```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.1-0.el7.x86_64.rpm +sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.2-0.el7.x86_64.rpm ``` ### Installation via Docker @@ -71,16 +71,16 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a ### Installation via Source Release Package -1. Create a directory named `apisix-2.10.1`. +1. Create a directory named `apisix-2.10.2`. ```shell - mkdir apisix-2.10.1 + mkdir apisix-2.10.2 ``` 2. Download Apache APISIX Release source package. ```shell - wget https://downloads.apache.org/apisix/2.10.1/apache-apisix-2.10.1-src.tgz + wget https://downloads.apache.org/apisix/2.10.2/apache-apisix-2.10.2-src.tgz ``` You can also download the Apache APISIX Release source package from the Apache APISIX website. The [Apache APISIX Official Website - Download Page](https://apisix.apache.org/downloads/) also provides source packages for Apache APISIX, APISIX Dashboard and APISIX Ingress Controller. @@ -88,14 +88,14 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a 3. Unzip the Apache APISIX Release source package. ```shell - tar zxvf apache-apisix-2.10.1-src.tgz -C apisix-2.10.1 + tar zxvf apache-apisix-2.10.2-src.tgz -C apisix-2.10.2 ``` 4. Install the runtime dependent Lua libraries. ```shell - # Switch to the apisix-2.10.1 directory - cd apisix-2.10.1 + # Switch to the apisix-2.10.2 directory + cd apisix-2.10.2 # Create dependencies make deps # Install apisix command diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index 4d683c8f8c4f..10c3790039ac 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -23,6 +23,7 @@ title: CHANGELOG ## Table of Contents +- [2.10.2](#2102) - [2.10.1](#2101) - [2.10.0](#2100) - [2.9.0](#290) @@ -47,6 +48,26 @@ title: CHANGELOG - [0.7.0](#070) - [0.6.0](#060) +## 2.10.2 + +### Bugfix + +- 更正 response.set_header 行为,与 request.set_header 保持一致 [#5499](https://github.com/apache/apisix/pull/5499) +- 修复 batch-requests 插件中 client ip 的问题 [#5476](https://github.com/apache/apisix/pull/5476) +- 修复 upstream 被多条 routes 绑定时,负载不平衡的问题 [#5462](https://github.com/apache/apisix/pull/5462) +- hmac-auth 插件检查是否缺少 X-HMAC-ALGORITHM header [#5467](https://github.com/apache/apisix/pull/5467) +- 防止不可信的 request_uri [#5458](https://github.com/apache/apisix/pull/5458) +- 修复用 PATCH 方法修改 boolean 参数的问题 [#5434](https://github.com/apache/apisix/pull/5432) +- auth-ldap 插件处理无效的 Authorization header [#5432](https://github.com/apache/apisix/pull/5432) +- 修复 traffic-split 插件 upstream_key 重复的问题 [#5414](https://github.com/apache/apisix/pull/5414) +- basic-auth 插件处理无效的 Authorization header [#5419](https://github.com/apache/apisix/pull/5419) +- 修复 install-dependencies.sh 中的依赖 [#5385](https://github.com/apache/apisix/pull/5385) +- 更正 request-validation 插件的 rejected_message 为 rejected_msg [#5355](https://github.com/apache/apisix/pull/5355) + +### Change + +- 只记录不敏感的 consumer 信息 [#5445](https://github.com/apache/apisix/pull/5445) + ## 2.10.1 ### Bugfix diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index d8ddedc55a0f..58cec16b50fe 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -1,5 +1,5 @@ { - "version": "2.10.1", + "version": "2.10.2", "sidebar": [ { "type": "category", diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index 83ea1821d8b1..738f73793f0a 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -58,7 +58,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep 这种安装方式适用于 CentOS 7 操作系统,请运行以下命令安装 Apache APISIX。 ```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.1-0.el7.x86_64.rpm +sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.2-0.el7.x86_64.rpm ``` ### 通过 Docker 安装 @@ -71,16 +71,16 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2 ### 通过源码包安装 -1. 创建一个名为 `apisix-2.10.1` 的目录。 +1. 创建一个名为 `apisix-2.10.2` 的目录。 ```shell - mkdir apisix-2.10.1 + mkdir apisix-2.10.2 ``` 2. 下载 Apache APISIX Release 源码包: ```shell - wget https://downloads.apache.org/apisix/2.10.1/apache-apisix-2.10.1-src.tgz + wget https://downloads.apache.org/apisix/2.10.2/apache-apisix-2.10.2-src.tgz ``` 您也可以通过 Apache APISIX 官网下载 Apache APISIX Release 源码包。 Apache APISIX 官网也提供了 Apache APISIX、APISIX Dashboard 和 APISIX Ingress Controller 的源码包,详情请参考[Apache APISIX 官网-下载页](https://apisix.apache.org/zh/downloads)。 @@ -88,14 +88,14 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2 3. 解压 Apache APISIX Release 源码包: ```shell - tar zxvf apache-apisix-2.10.1-src.tgz -C apisix-2.10.1 + tar zxvf apache-apisix-2.10.2-src.tgz -C apisix-2.10.2 ``` 4. 安装运行时依赖的 Lua 库: ```shell - # 切换到 apisix-2.10.1 目录 - cd apisix-2.10.1 + # 切换到 apisix-2.10.2 目录 + cd apisix-2.10.2 # 安装依赖 LUAROCKS_SERVER=https://luarocks.cn make deps # 安装 apisix 命令 diff --git a/rockspec/apisix-2.10.2-0.rockspec b/rockspec/apisix-2.10.2-0.rockspec new file mode 100644 index 000000000000..9f443aeebdb7 --- /dev/null +++ b/rockspec/apisix-2.10.2-0.rockspec @@ -0,0 +1,95 @@ +-- +-- 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. +-- + +package = "apisix" +version = "2.10.2-0" +supported_platforms = {"linux", "macosx"} + +source = { + url = "git://github.com/apache/apisix", + branch = "2.10.2", +} + +description = { + summary = "Apache APISIX is a cloud-native microservices API gateway, delivering the ultimate performance, security, open source and scalable platform for all your APIs and microservices.", + homepage = "https://github.com/apache/apisix", + license = "Apache License 2.0", +} + +dependencies = { + "lua-resty-ctxdump = 0.1-0", + "lua-resty-dns-client = 5.2.0", + "lua-resty-template = 2.0", + "lua-resty-etcd = 1.5.4", + "api7-lua-resty-http = 0.2.0", + "lua-resty-balancer = 0.04", + "lua-resty-ngxvar = 0.5.2", + "lua-resty-jit-uuid = 0.0.7", + "lua-resty-healthcheck-api7 = 2.2.0", + "lua-resty-jwt = 0.2.0", + "lua-resty-hmac-ffi = 0.05", + "lua-resty-cookie = 0.1.0", + "lua-resty-session = 2.24", + "opentracing-openresty = 0.1", + "lua-resty-radixtree = 2.8.1", + "lua-protobuf = 0.3.3", + "lua-resty-openidc = 1.7.2-1", + "luafilesystem = 1.7.0-2", + "api7-lua-tinyyaml = 0.3.0", + "nginx-lua-prometheus = 0.20210206", + "jsonschema = 0.9.5", + "lua-resty-ipmatcher = 0.6.1", + "lua-resty-kafka = 0.07", + "lua-resty-logger-socket = 2.0-0", + "skywalking-nginx-lua = 0.4-1", + "base64 = 1.5-2", + "binaryheap = 0.4", + "dkjson = 2.5-2", + "resty-redis-cluster = 1.02-4", + "lua-resty-expr = 1.3.1", + "graphql = 0.0.2", + "argparse = 0.7.1-1", + "luasocket = 3.0rc1-2", + "luasec = 0.9-1", + "lua-resty-consul = 0.3-2", + "penlight = 1.9.2-1", + "ext-plugin-proto = 0.3.0", + "casbin = 1.26.0", + "api7-snowflake = 2.0-1", + "inspect == 3.1.1", +} + +build = { + type = "make", + build_variables = { + CFLAGS="$(CFLAGS)", + LIBFLAG="$(LIBFLAG)", + LUA_LIBDIR="$(LUA_LIBDIR)", + LUA_BINDIR="$(LUA_BINDIR)", + LUA_INCDIR="$(LUA_INCDIR)", + LUA="$(LUA)", + OPENSSL_INCDIR="$(OPENSSL_INCDIR)", + OPENSSL_LIBDIR="$(OPENSSL_LIBDIR)", + }, + install_variables = { + INST_PREFIX="$(PREFIX)", + INST_BINDIR="$(BINDIR)", + INST_LIBDIR="$(LIBDIR)", + INST_LUADIR="$(LUADIR)", + INST_CONFDIR="$(CONFDIR)", + }, +} From 5b7209902874b6b9b84463783aaefebb5bd0ecdd Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 15 Nov 2021 01:35:42 -0600 Subject: [PATCH 098/260] ci: run CI on PR to release/** branch (#5509) --- .github/workflows/build.yml | 2 +- .github/workflows/centos7-ci.yml | 2 +- .github/workflows/chaos.yml | 3 +-- .github/workflows/cli.yml | 4 ++-- .github/workflows/code-lint.yml | 2 +- .github/workflows/doc-lint.yml | 2 +- .github/workflows/fuzzing-ci.yaml | 6 ++---- .github/workflows/license-checker.yml | 6 ++---- 8 files changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66e0c05100c8..d6a6a092e7d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: - 'docs/**' - '**/*.md' pull_request: - branches: [master] + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index dd8cb40c76b7..5f9f6ec742b1 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -7,7 +7,7 @@ on: - 'docs/**' - '**/*.md' pull_request: - branches: [master] + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index d4b82f0f5d40..3122de996134 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -2,8 +2,7 @@ name: Chaos Test on: pull_request: - branches: - - master + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index c8d749501683..e8d20300b5da 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -2,12 +2,12 @@ name: CLI Test on: push: - branches: [master] + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' pull_request: - branches: [master] + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index 4517c7985914..b5c6ade3e019 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -2,7 +2,7 @@ name: Code Lint on: pull_request: - branches: [master] + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 2bc2a9eb99c4..873a19afc010 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -2,7 +2,7 @@ name: Doc Lint on: pull_request: - branches: [master] + branches: [master, 'release/**'] paths: - '**/*.md' diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index 154b83cd12db..7fbc26552ccf 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -2,14 +2,12 @@ name: fuzzing on: push: - branches: - - master + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' pull_request: - branches: - - master + branches: [master, 'release/**'] paths-ignore: - 'docs/**' - '**/*.md' diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index 935552c657da..e1818c7f4ec9 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -20,11 +20,9 @@ name: License checker on: push: - branches: - - master + branches: [master, 'release/**'] pull_request: - branches: - - master + branches: [master, 'release/**'] jobs: check-license: From cc43b9fc1bf9be14c05d36415f83cdd189d0a7f5 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 15 Nov 2021 02:27:32 -0600 Subject: [PATCH 099/260] chore: remove ldap on release 2.10.2 (#5516) --- CHANGELOG.md | 2 -- docs/zh/latest/CHANGELOG.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcfb887e4a2..8c865379d724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,10 +58,8 @@ title: Changelog - fix(hmac-auth): check if the X-HMAC-ALGORITHM header is missing [#5467](https://github.com/apache/apisix/pull/5467) - fix: prevent being hacked by untrusted request_uri [#5458](https://github.com/apache/apisix/pull/5458) - fix(admin): modify boolean parameters with PATCH [#5434](https://github.com/apache/apisix/pull/5432) -- fix(auth-ldap): add handler for invalid basic auth header values [#5432](https://github.com/apache/apisix/pull/5432) - fix(traffic-split): multiple rules with multiple weighted_upstreams under each rule cause upstream_key duplicate [#5414](https://github.com/apache/apisix/pull/5414) - fix: add handler for invalid basic auth header values [#5419](https://github.com/apache/apisix/pull/5419) -- fix: ldap deps in install-dependencies.sh [#5385](https://github.com/apache/apisix/pull/5385) - fix(request-validation): correct rejected_message to rejected_msg [#5355](https://github.com/apache/apisix/pull/5355) ### Change diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index 10c3790039ac..44e981b45552 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -58,10 +58,8 @@ title: CHANGELOG - hmac-auth 插件检查是否缺少 X-HMAC-ALGORITHM header [#5467](https://github.com/apache/apisix/pull/5467) - 防止不可信的 request_uri [#5458](https://github.com/apache/apisix/pull/5458) - 修复用 PATCH 方法修改 boolean 参数的问题 [#5434](https://github.com/apache/apisix/pull/5432) -- auth-ldap 插件处理无效的 Authorization header [#5432](https://github.com/apache/apisix/pull/5432) - 修复 traffic-split 插件 upstream_key 重复的问题 [#5414](https://github.com/apache/apisix/pull/5414) - basic-auth 插件处理无效的 Authorization header [#5419](https://github.com/apache/apisix/pull/5419) -- 修复 install-dependencies.sh 中的依赖 [#5385](https://github.com/apache/apisix/pull/5385) - 更正 request-validation 插件的 rejected_message 为 rejected_msg [#5355](https://github.com/apache/apisix/pull/5355) ### Change From 718eb4b21efb2e640939f864441c5e84b7c46cad Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 15 Nov 2021 19:34:57 -0600 Subject: [PATCH 100/260] chore: remove unnecessary backport PR in CHANGELOG.md (#5519) --- CHANGELOG.md | 1 - docs/zh/latest/CHANGELOG.md | 1 - 2 files changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c865379d724..752cb0e9f806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,6 @@ title: Changelog - fix(admin): modify boolean parameters with PATCH [#5434](https://github.com/apache/apisix/pull/5432) - fix(traffic-split): multiple rules with multiple weighted_upstreams under each rule cause upstream_key duplicate [#5414](https://github.com/apache/apisix/pull/5414) - fix: add handler for invalid basic auth header values [#5419](https://github.com/apache/apisix/pull/5419) -- fix(request-validation): correct rejected_message to rejected_msg [#5355](https://github.com/apache/apisix/pull/5355) ### Change diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index 44e981b45552..031cd233ec22 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -60,7 +60,6 @@ title: CHANGELOG - 修复用 PATCH 方法修改 boolean 参数的问题 [#5434](https://github.com/apache/apisix/pull/5432) - 修复 traffic-split 插件 upstream_key 重复的问题 [#5414](https://github.com/apache/apisix/pull/5414) - basic-auth 插件处理无效的 Authorization header [#5419](https://github.com/apache/apisix/pull/5419) -- 更正 request-validation 插件的 rejected_message 为 rejected_msg [#5355](https://github.com/apache/apisix/pull/5355) ### Change From 9b355c6f13194c00eeb491542ee0727467988877 Mon Sep 17 00:00:00 2001 From: Baoyuan Date: Tue, 16 Nov 2021 00:27:39 -0600 Subject: [PATCH 101/260] docs: update batch-processor.md (#5525) --- docs/zh/latest/batch-processor.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/zh/latest/batch-processor.md b/docs/zh/latest/batch-processor.md index 4071bfd96dfc..95d8fe78eba9 100644 --- a/docs/zh/latest/batch-processor.md +++ b/docs/zh/latest/batch-processor.md @@ -1,5 +1,5 @@ --- -title: 批处理机 +title: 批处理器 --- -批处理处理器可用于聚合条目(日志/任何数据)并进行批处理。 -当batch_max_size设置为零时,处理器将立即执行每个条目。将批处理的最大大小设置为大于1将开始聚合条目,直到达到最大大小或超时到期为止 +批处理器可用于聚合条目(日志/任何数据)并进行批处理。 +当 `batch_max_size` 设置为零时,处理器将立即执行每个条目。将批处理的最大值设置为大于 1 将开始聚合条目,直到达到最大值或超时。 -## 构型 +## 配置 -创建批处理程序的唯一必需参数是函数。当批处理达到最大大小或缓冲区持续时间超过时,将执行该功能。 +创建批处理器的唯一必需参数是函数。当批处理达到最大值或缓冲区持续时间超过时,函数将被执行。 -|名称 |需求 |描述| +|名称 |必选项 |描述| |------- |----- |------| |name |可选的 |标识批处理者的唯一标识符| -|batch_max_size |可选的 |每批的最大大小,默认为1000| -|inactive_timeout|可选的 |如果不活动,将刷新缓冲区的最大时间(以秒为单位),默认值为5s| -|buffer_duration|可选的 |必须先处理批次中最旧条目的最大期限(以秒为单位),默认是5| -|max_retry_count|可选的 |从处理管道中移除之前的最大重试次数;默认为零| -|retry_delay |可选的 |如果执行失败,应该延迟进程执行的秒数;默认为1| +|batch_max_size |可选的 |每批的最大大小,默认为 `1000`| +|inactive_timeout|可选的 |如果不活动,将刷新缓冲区的最大时间(以秒为单位),默认值为 `5`| +|buffer_duration|可选的 |必须先处理批次中最旧条目的最大期限(以秒为单位),默认是 `5`| +|max_retry_count|可选的 |从处理管道中移除之前的最大重试次数;默认为 `0`| +|retry_delay |可选的 |如果执行失败,应该延迟进程执行的秒数;默认为 `1`| -以下代码显示了如何使用批处理程序的示例。批处理处理器将要执行的功能作为第一个参数,将批处理配置作为第二个参数。 +以下代码显示了如何使用批处理程序的示例。批处理器将一个要执行的函数作为第一个参数,将批处理配置作为第二个参数。 ```lua local bp = require("apisix.utils.batch-processor") @@ -63,6 +63,6 @@ if batch_processor then end ``` -注意:请确保批处理的最大大小(条目数)在函数执行的范围内。 -刷新批处理的计时器基于“ inactive_timeout”配置运行。因此,为了获得最佳使用效果, -保持“ inactive_timeout”小于“ buffer_duration”。 +注意:请确保批处理的最大值(条目数)在函数执行的范围内。 +刷新批处理的计时器基于 `inactive_timeout` 配置运行。因此,为了获得最佳使用效果, +保持 `inactive_timeout` 小于 `buffer_duration`。 From 2c12b3695302f580a1498333aaafb68da26a3096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 16 Nov 2021 18:00:03 +0800 Subject: [PATCH 102/260] fix(wasm): the conf can't be empty (#5514) --- apisix/wasm.lua | 3 ++- docs/en/latest/wasm.md | 2 +- t/wasm/route.t | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apisix/wasm.lua b/apisix/wasm.lua index c99731a99aaa..9e3e9293f939 100644 --- a/apisix/wasm.lua +++ b/apisix/wasm.lua @@ -22,7 +22,8 @@ local schema = { type = "object", properties = { conf = { - type = "string" + type = "string", + minLength = 1, }, }, required = {"conf"} diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md index 5f13d7ea1bfc..9606bd8f5b92 100644 --- a/docs/en/latest/wasm.md +++ b/docs/en/latest/wasm.md @@ -93,4 +93,4 @@ Attributes below can be configured in the plugin: | Name | Type | Requirement | Default | Valid | Description | | --------------------------------------| ------------| -------------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| conf | string | required | | | the plugin ctx configuration which can be fetched via Proxy WASM SDK | +| conf | string | required | | != "" | the plugin ctx configuration which can be fetched via Proxy WASM SDK | diff --git a/t/wasm/route.t b/t/wasm/route.t index 69f43d39a954..cb348011c1a7 100644 --- a/t/wasm/route.t +++ b/t/wasm/route.t @@ -65,6 +65,9 @@ __DATA__ {input = { conf = {} }}, + {input = { + conf = "" + }}, }) do local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, @@ -87,6 +90,7 @@ __DATA__ --- response_body failed to check the configuration of plugin wasm_log err: property "conf" is required failed to check the configuration of plugin wasm_log err: property "conf" validation failed: wrong type: expected string, got table +failed to check the configuration of plugin wasm_log err: property "conf" validation failed: string too short, expected at least 1, got 0 From 5394dce6fb9dde973baee41a31107d99ab2ca1f3 Mon Sep 17 00:00:00 2001 From: windyrjc Date: Thu, 18 Nov 2021 09:48:14 +0800 Subject: [PATCH 103/260] feat(kafka-logger): supports logging request body (#5501) Co-authored-by: windyrjc Co-authored-by: yundian --- apisix/plugins/kafka-logger.lua | 20 +++++++ apisix/utils/log-util.lua | 38 ++++++++++--- docs/en/latest/plugins/kafka-logger.md | 1 + docs/zh/latest/plugins/kafka-logger.md | 1 + t/plugin/kafka-logger.t | 79 ++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 7 deletions(-) diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua index b680bd4c36f6..f045c3958e15 100644 --- a/apisix/plugins/kafka-logger.lua +++ b/apisix/plugins/kafka-logger.lua @@ -19,6 +19,7 @@ local log_util = require("apisix.utils.log-util") local producer = require ("resty.kafka.producer") local batch_processor = require("apisix.utils.batch-processor") local plugin = require("apisix.plugin") +local expr = require("resty.expr.v1") local math = math local pairs = pairs @@ -74,6 +75,16 @@ local schema = { inactive_timeout = {type = "integer", minimum = 1, default = 5}, batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false}, + include_req_body_expr = { + type = "array", + minItems = 1, + items = { + type = "array", + items = { + type = "string" + } + } + }, -- in lua-resty-kafka, cluster_name is defined as number -- see https://github.com/doujiang24/lua-resty-kafka#new-1 cluster_name = {type = "integer", minimum = 1, default = 1}, @@ -98,6 +109,15 @@ local _M = { function _M.check_schema(conf, schema_type) + + if conf.include_req_body_expr then + local ok, err = expr.new(conf.include_req_body_expr) + if not ok then + return nil, + {error_msg = "failed to validate the 'include_req_body_expr' expression: " .. err} + end + end + if schema_type == core.schema.TYPE_METADATA then return core.schema.check(metadata_schema, conf) end diff --git a/apisix/utils/log-util.lua b/apisix/utils/log-util.lua index 10620d1b7387..9216ba50cb97 100644 --- a/apisix/utils/log-util.lua +++ b/apisix/utils/log-util.lua @@ -15,6 +15,7 @@ -- limitations under the License. -- local core = require("apisix.core") +local expr = require("resty.expr.v1") local ngx = ngx local pairs = pairs local str_byte = string.byte @@ -119,13 +120,36 @@ local function get_full_log(ngx, conf) } if conf.include_req_body then - local body = req_get_body_data() - if body then - log.request.body = body - else - local body_file = ngx.req.get_body_file() - if body_file then - log.request.body_file = body_file + + local log_request_body = true + + if conf.include_req_body_expr then + + if not conf.request_expr then + local request_expr, err = expr.new(conf.include_req_body_expr) + if not request_expr then + core.log.error('generate log expr err ' .. err) + return log + end + conf.request_expr = request_expr + end + + local result = conf.request_expr:eval(ctx.var) + + if not result then + log_request_body = false + end + end + + if log_request_body then + local body = req_get_body_data() + if body then + log.request.body = body + else + local body_file = ngx.req.get_body_file() + if body_file then + log.request.body_file = body_file + end end end end diff --git a/docs/en/latest/plugins/kafka-logger.md b/docs/en/latest/plugins/kafka-logger.md index f19a693a4f38..fd6fc2495f0f 100644 --- a/docs/en/latest/plugins/kafka-logger.md +++ b/docs/en/latest/plugins/kafka-logger.md @@ -57,6 +57,7 @@ For more info on Batch-Processor in Apache APISIX please refer. | max_retry_count | integer | optional | 0 | [0,...] | Maximum number of retries before removing from the processing pipe line. | | retry_delay | integer | optional | 1 | [0,...] | Number of seconds the process execution should be delayed if the execution fails. | | include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. | +| include_req_body_expr | array | optional | | | Whether to logging request body, based on [lua-resty-expr](https://github.com/api7/lua-resty-expr), this option require to turn on `include_req_body` option. | | cluster_name | integer | optional | 1 | [0,...] | the name of the cluster. When there are two or more kafka clusters, you can specify different names. And this only works with async producer_type.| ### examples of meta_format diff --git a/docs/zh/latest/plugins/kafka-logger.md b/docs/zh/latest/plugins/kafka-logger.md index 75cadf18aaec..fc2204fb31f8 100644 --- a/docs/zh/latest/plugins/kafka-logger.md +++ b/docs/zh/latest/plugins/kafka-logger.md @@ -57,6 +57,7 @@ title: kafka-logger | max_retry_count | integer | 可选 | 0 | [0,...] | 从处理管道中移除之前的最大重试次数。 | | retry_delay | integer | 可选 | 1 | [0,...] | 如果执行失败,则应延迟执行流程的秒数。 | | include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ; true: 表示包含请求的 body 。| +| include_req_body_expr | array | 可选 | | | 是否采集请求body, 基于[lua-resty-expr](https://github.com/api7/lua-resty-expr)。 该选项需要开启 `include_req_body`| | cluster_name | integer | 可选 | 1 | [0,...] | kafka 集群的名称。当有两个或多个 kafka 集群时,可以指定不同的名称。只适用于 producer_type 是 async 模式。| ### meta_format 参考示例 diff --git a/t/plugin/kafka-logger.t b/t/plugin/kafka-logger.t index 51876d30658a..5094910f37ea 100644 --- a/t/plugin/kafka-logger.t +++ b/t/plugin/kafka-logger.t @@ -1114,3 +1114,82 @@ GET /t --- error_log_like eval qr/create new kafka producer instance, brokers: \[\{"port":9092,"host":"127.0.0.127"}]/ qr/failed to send data to Kafka topic: .*, brokers: \{"127.0.0.127":9092}/ + + + +=== TEST 26: set route(id: 1,include_req_body = true,include_req_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "kafka-logger": { + "broker_list" : + { + "127.0.0.1":9092 + }, + "kafka_topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_req_body": true, + "include_req_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 27: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log eval +qr/send data to kafka: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 28: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to kafka: \{.*"body":"abcdef"/ +--- wait: 2 From 7c4f10e445618e30c538a6edce9ffd0da09b0816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 18 Nov 2021 11:57:56 +0800 Subject: [PATCH 104/260] chore: check the error from get_post_args (#5537) --- apisix/core/request.lua | 7 ++++++- t/core/request.t | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apisix/core/request.lua b/apisix/core/request.lua index f098f63280b3..7d9bf8090e60 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -161,7 +161,12 @@ function _M.get_post_args(ctx) -- use 0 to avoid truncated result and keep the behavior as the -- same as other platforms - local args = req_get_post_args(0) + local args, err = req_get_post_args(0) + if not args then + -- do we need a way to handle huge post forms? + log.error("the post form is too large: ", err) + args = {} + end ctx.req_post_args = args end diff --git a/t/core/request.t b/t/core/request.t index e9dca7b8f1b9..06256dcc9a4b 100644 --- a/t/core/request.t +++ b/t/core/request.t @@ -408,3 +408,32 @@ z_z x x --- no_error_log [error] + + + +=== TEST 12: get_post_args when the body is stored in temp file +--- config + location = /hello { + client_body_in_file_only clean; + content_by_lua_block { + local core = require("apisix.core") + local ngx_ctx = ngx.ctx + local api_ctx = ngx_ctx.api_ctx + if api_ctx == nil then + api_ctx = core.tablepool.fetch("api_ctx", 0, 32) + ngx_ctx.api_ctx = api_ctx + end + + core.ctx.set_vars_meta(api_ctx) + + local args = core.request.get_post_args(ngx.ctx.api_ctx) + ngx.say(args["c"]) + } + } +--- request +POST /hello +c=z_z&v=x%20x +--- response_body +nil +--- error_log +the post form is too large: request body in temp file not supported From 1018576e30477a9d1e70710386aac998a68acc68 Mon Sep 17 00:00:00 2001 From: cache-missing <90820067+cache-missing@users.noreply.github.com> Date: Thu, 18 Nov 2021 14:18:19 +0800 Subject: [PATCH 105/260] fix: str concat in error call (#5540) Co-authored-by: kaihaojiang --- apisix/plugin.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index efdffe47d40e..b1b7d660c440 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -264,7 +264,7 @@ function _M.load(config) local_conf, err = core.config.local_conf(true) if not local_conf then -- the error is unrecoverable, so we need to raise it - error("failed to load the configuration file: ", err) + error("failed to load the configuration file: " .. err) end http_plugin_names = local_conf.plugins From 3a6a4db281658e5b9b832332a62e2893f0e57280 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Fri, 19 Nov 2021 08:01:20 +0530 Subject: [PATCH 106/260] feat(plugin): azure serverless functions (#5479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- apisix/plugins/azure-functions.lua | 137 ++++++++ conf/config-default.yaml | 1 + docs/en/latest/config.json | 3 +- docs/en/latest/plugins/azure-functions.md | 140 ++++++++ t/admin/plugins.t | 2 +- t/lib/test_admin.lua | 6 +- t/plugin/azure-functions.t | 377 ++++++++++++++++++++++ 7 files changed, 661 insertions(+), 5 deletions(-) create mode 100644 apisix/plugins/azure-functions.lua create mode 100644 docs/en/latest/plugins/azure-functions.md create mode 100644 t/plugin/azure-functions.t diff --git a/apisix/plugins/azure-functions.lua b/apisix/plugins/azure-functions.lua new file mode 100644 index 000000000000..1597f2aab238 --- /dev/null +++ b/apisix/plugins/azure-functions.lua @@ -0,0 +1,137 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local core = require("apisix.core") +local http = require("resty.http") +local plugin = require("apisix.plugin") +local ngx = ngx +local plugin_name = "azure-functions" + +local schema = { + type = "object", + properties = { + function_uri = {type = "string"}, + authorization = { + type = "object", + properties = { + apikey = {type = "string"}, + clientid = {type = "string"} + } + }, + timeout = {type = "integer", minimum = 100, default = 3000}, + ssl_verify = {type = "boolean", default = true}, + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 5} + }, + required = {"function_uri"} +} + +local metadata_schema = { + type = "object", + properties = { + master_apikey = {type = "string", default = ""}, + master_clientid = {type = "string", default = ""} + } +} + +local _M = { + version = 0.1, + priority = -1900, + name = plugin_name, + schema = schema, + metadata_schema = metadata_schema +} + +function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + end + return core.schema.check(schema, conf) +end + +function _M.access(conf, ctx) + local uri_args = core.request.get_uri_args(ctx) + local headers = core.request.headers(ctx) or {} + local req_body, err = core.request.get_body() + + if err then + core.log.error("error while reading request body: ", err) + return 400 + end + + -- set authorization headers if not already set by the client + -- we are following not to overwrite the authz keys + if not headers["x-functions-key"] and + not headers["x-functions-clientid"] then + if conf.authorization then + headers["x-functions-key"] = conf.authorization.apikey + headers["x-functions-clientid"] = conf.authorization.clientid + else + -- If neither api keys are set with the client request nor inside the plugin attributes + -- plugin will fallback to the master key (if any) present inside the metadata. + local metadata = plugin.plugin_metadata(plugin_name) + if metadata then + headers["x-functions-key"] = metadata.value.master_apikey + headers["x-functions-clientid"] = metadata.value.master_clientid + end + end + end + + headers["host"] = nil + local params = { + method = ngx.req.get_method(), + body = req_body, + query = uri_args, + headers = headers, + keepalive = conf.keepalive, + ssl_verify = conf.ssl_verify + } + + -- Keepalive options + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + end + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local res, err = httpc:request_uri(conf.function_uri, params) + + if not res or err then + core.log.error("failed to process azure function, err: ", err) + return 503 + end + + -- According to RFC7540 https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2, endpoint + -- must not generate any connection specific headers for HTTP/2 requests. + local response_headers = res.headers + if ngx.var.http2 then + response_headers["Connection"] = nil + response_headers["Keep-Alive"] = nil + response_headers["Proxy-Connection"] = nil + response_headers["Upgrade"] = nil + response_headers["Transfer-Encoding"] = nil + end + + -- setting response headers + core.response.set_header(response_headers) + + return res.status, res.body +end + +return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index e0ea36f22575..e678a0a21e80 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -354,6 +354,7 @@ plugins: # plugin list (sorted by priority) # <- recommend to use priority (0, 100) for your custom plugins - example-plugin # priority: 0 #- skywalking # priority: -1100 + - azure-functions # priority: -1900 - serverless-post-function # priority: -2000 - ext-plugin-post-req # priority: -3000 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index cb45e103c3e3..67e35a013a11 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -127,7 +127,8 @@ "type": "category", "label": "Serverless", "items": [ - "plugins/serverless" + "plugins/serverless", + "plugins/azure-functions" ] }, { diff --git a/docs/en/latest/plugins/azure-functions.md b/docs/en/latest/plugins/azure-functions.md new file mode 100644 index 000000000000..a999a50fb3e2 --- /dev/null +++ b/docs/en/latest/plugins/azure-functions.md @@ -0,0 +1,140 @@ +--- +title: azure-functions +--- + + + +## Summary + +- [Summary](#summary) +- [Name](#name) +- [Attributes](#attributes) +- [Metadata](#metadata) +- [How To Enable](#how-to-enable) +- [Disable Plugin](#disable-plugin) + +## Name + +`azure-functions` is a serverless plugin built into Apache APISIX for seamless integration with [Azure Serverless Function](https://azure.microsoft.com/en-in/services/functions/) as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud, one of the most used public cloud platforms for production environment. If enabled, this plugin terminates the ongoing request to that particular URI and initiates a new request to the azure faas (the new upstream) on behalf of the client with the suitable authorization details set by the users, request headers, request body, params ( all these three components are passed from the original request ) and returns the response body, status code and the headers back to the original client that has invoked the request to the APISIX agent. + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | +| function_uri | string | required | | | The azure function endpoint which triggers the serverless function code (eg. http://test-apisix.azurewebsites.net/api/HttpTrigger). | +| authorization | object | optional | | | Authorization credentials to access the cloud function. | +| authorization.apikey | string | optional | | | Field inside _authorization_. The generate API Key to authorize requests to that endpoint. | | +| authorization.clientid | string | optional | | | Field inside _authorization_. The Client ID ( azure active directory ) to authorize requests to that endpoint. | | +| timeout | integer | optional | 3000 | [100,...] | Proxy request timeout in milliseconds. | +| ssl_verify | boolean | optional | true | true/false | If enabled performs SSL verification of the server. | +| keepalive | boolean | optional | true | true/false | To reuse the same proxy connection in near future. Set to false to disable keepalives and immediately close the connection. | +| keepalive_pool | integer | optional | 5 | [1,...] | The maximum number of connections in the pool. | +| keepalive_timeout | integer | optional | 60000 | [1000,...] | The maximal idle timeout (ms). | + +## Metadata + +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------- | +| master_apikey | string | optional | "" | | The API KEY secret that could be used to access the azure function uri. | +| master_clientid | string | optional | "" | | The Client ID (active directory) that could be used the authorize the function uri | + +Metadata for `azure-functions` plugin provides the functionality for authorization fallback. It defines `master_apikey` and `master_clientid` (azure active directory client id) where users (optionally) can define the master API key or Client ID for mission-critical application deployment. So if there are no authorization details found inside the plugin attribute the authorization details present in the metadata kicks in. + +The relative priority ordering is as follows: + +- First, the plugin looks for `x-functions-key` or `x-functions-clientid` keys inside the request header to the APISIX agent. +- If they are not found, the azure-functions plugin checks for the authorization details inside plugin attributes. If present, it adds the respective header to the request sent to the Azure cloud function. +- If no authorization details are found inside plugin attributes, APISIX fetches the metadata config for this plugin and uses the master keys. + +To add a new Master APIKEY, make a request to _/apisix/admin/plugin_metadata_ endpoint with the updated metadata as follows: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/azure-functions -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "master_apikey" : "" +}' +``` + +## How To Enable + +The following is an example of how to enable the azure-function faas plugin for a specific route URI. We are assuming your cloud function is already up and running. + +```shell +# enable azure function for a route +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "azure-functions": { + "function_uri": "http://test-apisix.azurewebsites.net/api/HttpTrigger", + "authorization": { + "apikey": "" + } + } + }, + "uri": "/azure" +}' +``` + +Now any requests (HTTP/1.1, HTTPS, HTTP2) to URI `/azure` will trigger an HTTP invocation to the aforesaid function URI and response body along with the response headers and response code will be proxied back to the client. For example ( here azure cloud function just take the `name` query param and returns `Hello $name` ) : + +```shell +$ curl -i -XGET http://localhost:9080/azure\?name=apisix +HTTP/1.1 200 OK +Content-Type: text/plain; charset=utf-8 +Transfer-Encoding: chunked +Connection: keep-alive +Request-Context: appId=cid-v1:38aae829-293b-43c2-82c6-fa94aec0a071 +Date: Wed, 17 Nov 2021 14:46:55 GMT +Server: APISIX/2.10.2 + +Hello, apisix +``` + +For requests where the mode of communication between the client and the Apache APISIX gateway is HTTP/2, the example looks like ( make sure you are running APISIX agent with `enable_http2: true` for a port in conf.yaml or uncomment port 9081 of `node_listen` field inside [config-default.yaml](../../../../conf/config-default.yaml) ) : + +```shell +$ curl -i -XGET --http2 --http2-prior-knowledge http://localhost:9081/azure\?name=apisix +HTTP/2 200 +content-type: text/plain; charset=utf-8 +request-context: appId=cid-v1:38aae829-293b-43c2-82c6-fa94aec0a071 +date: Wed, 17 Nov 2021 14:54:07 GMT +server: APISIX/2.10.2 + +Hello, apisix +``` + +## Disable Plugin + +Remove the corresponding JSON configuration in the plugin configuration to disable the `azure-functions` plugin and add the suitable upstream configuration. +APISIX plugins are hot-reloaded, therefore no need to restart APISIX. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/azure", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 8e69ef8a294b..4821717a6a94 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -40,7 +40,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","serverless-post-function","ext-plugin-post-req"\]/ +qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","azure-functions","serverless-post-function","ext-plugin-post-req"\]/ --- no_error_log [error] diff --git a/t/lib/test_admin.lua b/t/lib/test_admin.lua index 5553d5047e65..624c768c3f01 100644 --- a/t/lib/test_admin.lua +++ b/t/lib/test_admin.lua @@ -197,11 +197,11 @@ function _M.test(uri, method, body, pattern, headers) end if res.status >= 300 then - return res.status, res.body + return res.status, res.body, res.headers end if pattern == nil then - return res.status, "passed", res.body + return res.status, "passed", res.body, res.headers end local res_data = json.decode(res.body) @@ -210,7 +210,7 @@ function _M.test(uri, method, body, pattern, headers) return 500, "failed, " .. err, res_data end - return 200, "passed", res_data + return 200, "passed", res_data, res.headers end diff --git a/t/plugin/azure-functions.t b/t/plugin/azure-functions.t new file mode 100644 index 000000000000..ea4d0649f684 --- /dev/null +++ b/t/plugin/azure-functions.t @@ -0,0 +1,377 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $inside_lua_block = $block->inside_lua_block // ""; + chomp($inside_lua_block); + my $http_config = $block->http_config // <<_EOC_; + + server { + listen 8765; + + location /httptrigger { + content_by_lua_block { + ngx.req.read_body() + local msg = "faas invoked" + ngx.header['Content-Length'] = #msg + 1 + ngx.header['X-Extra-Header'] = "MUST" + ngx.header['Connection'] = "Keep-Alive" + ngx.say(msg) + } + } + + location /azure-demo { + content_by_lua_block { + $inside_lua_block + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.azure-functions") + local conf = { + function_uri = "http://some-url.com" + } + local ok, err = plugin.check_schema(conf) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 2: function_uri missing +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.azure-functions") + local ok, err = plugin.check_schema({}) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +property "function_uri" is required + + + +=== TEST 3: create route with azure-function plugin enabled +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "azure-functions": { + "function_uri": "http://localhost:8765/httptrigger" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/azure" + }]], + [[{ + "node": { + "value": { + "plugins": { + "azure-functions": { + "keepalive": true, + "timeout": 3000, + "ssl_verify": true, + "keepalive_timeout": 60000, + "keepalive_pool": 5, + "function_uri": "http://localhost:8765/httptrigger" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/azure" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: Test plugin endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local core = require("apisix.core") + + local code, _, body, headers = t("/azure", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + -- headers proxied 2 times -- one by plugin, another by this test case + core.response.set_header(headers) + ngx.print(body) + } + } +--- response_body +faas invoked +--- response_headers +Content-Length: 13 +X-Extra-Header: MUST + + + +=== TEST 5: http2 check response body and headers +--- http2 +--- request +GET /azure +--- response_body +faas invoked + + + +=== TEST 6: check HTTP/2 response headers (must not contain any connection specific info) +First fetch the header from curl with -I then check the count of Connection +The full header looks like the format shown below + +HTTP/2 200 +content-type: text/plain +x-extra-header: MUST +content-length: 13 +date: Wed, 17 Nov 2021 13:53:08 GMT +server: APISIX/2.10.2 + +--- http2 +--- request +HEAD /azure +--- response_headers +Connection: +Upgrade: +Keep-Alive: +content-type: text/plain +x-extra-header: MUST +content-length: 13 + + + +=== TEST 7: check authz header +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- passing an apikey + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "azure-functions": { + "function_uri": "http://localhost:8765/azure-demo", + "authorization": { + "apikey": "test_key" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/azure" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + local code, _, body = t("/azure", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.print(body) + } + } +--- inside_lua_block +local headers = ngx.req.get_headers() or {} +ngx.say("Authz-Header - " .. headers["x-functions-key"] or "") + +--- response_body +passed +Authz-Header - test_key + + + +=== TEST 8: check if apikey doesn't get overrided passed by client to the gateway +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local header = {} + header["x-functions-key"] = "must_not_be_overrided" + + -- plugin schema already contains apikey with value "test_key" which won't be respected + local code, _, body = t("/azure", "GET", nil, nil, header) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.print(body) + } + } +--- inside_lua_block +local headers = ngx.req.get_headers() or {} +ngx.say("Authz-Header - " .. headers["x-functions-key"] or "") + +--- response_body +Authz-Header - must_not_be_overrided + + + +=== TEST 9: fall back to metadata master key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, meta_body = t('/apisix/admin/plugin_metadata/azure-functions', + ngx.HTTP_PUT, + [[{ + "master_apikey":"metadata_key" + }]], + [[{ + "node": { + "value": { + "master_apikey": "metadata_key", + "master_clientid": "" + }, + "key": "/apisix/plugin_metadata/azure-functions" + }, + "action": "set" + }]]) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + ngx.say(meta_body) + + -- update plugin attribute + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "azure-functions": { + "function_uri": "http://localhost:8765/azure-demo" + } + }, + "uri": "/azure" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + -- plugin schema already contains apikey with value "test_key" which won't be respected + local code, _, body = t("/azure", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.print(body) + } + } +--- inside_lua_block +local headers = ngx.req.get_headers() or {} +ngx.say("Authz-Header - " .. headers["x-functions-key"] or "") + +--- response_body +passed +passed +Authz-Header - metadata_key From b9ecafab5f1c1e59bf37339daa783ee45ec3666c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 19 Nov 2021 17:03:21 +0800 Subject: [PATCH 107/260] docs(kafka-logger): explain when the request body can't be logged (#5552) --- docs/en/latest/plugins/kafka-logger.md | 4 ++-- docs/zh/latest/plugins/kafka-logger.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/latest/plugins/kafka-logger.md b/docs/en/latest/plugins/kafka-logger.md index fd6fc2495f0f..9aa3d92559a7 100644 --- a/docs/en/latest/plugins/kafka-logger.md +++ b/docs/en/latest/plugins/kafka-logger.md @@ -56,8 +56,8 @@ For more info on Batch-Processor in Apache APISIX please refer. | buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed.| | max_retry_count | integer | optional | 0 | [0,...] | Maximum number of retries before removing from the processing pipe line. | | retry_delay | integer | optional | 1 | [0,...] | Number of seconds the process execution should be delayed if the execution fails. | -| include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. | -| include_req_body_expr | array | optional | | | Whether to logging request body, based on [lua-resty-expr](https://github.com/api7/lua-resty-expr), this option require to turn on `include_req_body` option. | +| include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. Note: if the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitation. | +| include_req_body_expr | array | optional | | | When `include_req_body` is true, control the behavior based on the result of the [lua-resty-expr](https://github.com/api7/lua-resty-expr) expression. If present, only log the request body when the result is true. | | cluster_name | integer | optional | 1 | [0,...] | the name of the cluster. When there are two or more kafka clusters, you can specify different names. And this only works with async producer_type.| ### examples of meta_format diff --git a/docs/zh/latest/plugins/kafka-logger.md b/docs/zh/latest/plugins/kafka-logger.md index fc2204fb31f8..0819bd62408d 100644 --- a/docs/zh/latest/plugins/kafka-logger.md +++ b/docs/zh/latest/plugins/kafka-logger.md @@ -56,8 +56,8 @@ title: kafka-logger | buffer_duration | integer | 可选 | 60 | [1,...] | 必须先处理批次中最旧条目的最长期限(以秒为单位)。 | | max_retry_count | integer | 可选 | 0 | [0,...] | 从处理管道中移除之前的最大重试次数。 | | retry_delay | integer | 可选 | 1 | [0,...] | 如果执行失败,则应延迟执行流程的秒数。 | -| include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ; true: 表示包含请求的 body 。| -| include_req_body_expr | array | 可选 | | | 是否采集请求body, 基于[lua-resty-expr](https://github.com/api7/lua-resty-expr)。 该选项需要开启 `include_req_body`| +| include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ;true: 表示包含请求的 body。注意:如果请求 body 没办法完全放在内存中,由于 Nginx 的限制,我们没有办法把它记录下来。| +| include_req_body_expr | array | 可选 | | | 当 `include_req_body` 开启时, 基于 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 表达式的结果进行记录。如果该选项存在,只有在表达式为真的时候才会记录请求 body。 | | cluster_name | integer | 可选 | 1 | [0,...] | kafka 集群的名称。当有两个或多个 kafka 集群时,可以指定不同的名称。只适用于 producer_type 是 async 模式。| ### meta_format 参考示例 From fc5d70ac68d604bea804e1f4d5fd905ba791eefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 19 Nov 2021 17:37:38 +0800 Subject: [PATCH 108/260] feat(wasm): run in http header_filter (#5544) --- apisix/wasm.lua | 20 ++++++- docs/en/latest/wasm.md | 9 +++- t/wasm/response-rewrite.t | 94 +++++++++++++++++++++++++++++++++ t/wasm/response-rewrite/main.go | 89 +++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 t/wasm/response-rewrite.t create mode 100644 t/wasm/response-rewrite/main.go diff --git a/apisix/wasm.lua b/apisix/wasm.lua index 9e3e9293f939..939549a2ad9f 100644 --- a/apisix/wasm.lua +++ b/apisix/wasm.lua @@ -65,7 +65,7 @@ end local function access_wrapper(self, conf, ctx) local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin) if not plugin_ctx then - core.log.error("failed to init wasm plugin ctx: ", err) + core.log.error("failed to fetch wasm plugin ctx: ", err) return 503 end @@ -77,6 +77,21 @@ local function access_wrapper(self, conf, ctx) end +local function header_filter_wrapper(self, conf, ctx) + local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin) + if not plugin_ctx then + core.log.error("failed to fetch wasm plugin ctx: ", err) + return 503 + end + + local ok, err = wasm.on_http_response_headers(plugin_ctx) + if not ok then + core.log.error("failed to run wasm plugin: ", err) + return 503 + end +end + + function _M.require(attrs) if not support_wasm then return nil, "need to build APISIX-OpenResty to support wasm" @@ -101,6 +116,9 @@ function _M.require(attrs) mod.access = function (conf, ctx) return access_wrapper(mod, conf, ctx) end + mod.header_filter = function (conf, ctx) + return header_filter_wrapper(mod, conf, ctx) + end -- the returned values need to be the same as the Lua's 'require' return true, mod diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md index 9606bd8f5b92..8d81d69a2e00 100644 --- a/docs/en/latest/wasm.md +++ b/docs/en/latest/wasm.md @@ -93,4 +93,11 @@ Attributes below can be configured in the plugin: | Name | Type | Requirement | Default | Valid | Description | | --------------------------------------| ------------| -------------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| conf | string | required | | != "" | the plugin ctx configuration which can be fetched via Proxy WASM SDK | +| conf | string | required | | != "" | the plugin ctx configuration which can be fetched via Proxy WASM SDK | + +Here is the mapping between Proxy WASM callbacks and APISIX's phases: + +* `proxy_on_configure`: run once there is not PluginContext for the new configuration. +For example, when the first request hits the route which has WASM plugin configured. +* `proxy_on_http_request_headers`: run in the access phase. +* `proxy_on_http_response_headers`: run in the header_filter phase. diff --git a/t/wasm/response-rewrite.t b/t/wasm/response-rewrite.t new file mode 100644 index 000000000000..250a07c10fce --- /dev/null +++ b/t/wasm/response-rewrite.t @@ -0,0 +1,94 @@ +# +# 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; + +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'); +} + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $extra_yaml_config = <<_EOC_; +wasm: + plugins: + - name: wasm-response-rewrite + priority: 7997 + file: t/wasm/response-rewrite/main.go.wasm +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: response rewrite headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm-response-rewrite": { + "conf": "{\"headers\":[{\"name\":\"x-wasm\",\"value\":\"apisix\"}]}" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit +--- request +GET /hello +--- response_headers +x-wasm: apisix diff --git a/t/wasm/response-rewrite/main.go b/t/wasm/response-rewrite/main.go new file mode 100644 index 000000000000..1c469aa22d39 --- /dev/null +++ b/t/wasm/response-rewrite/main.go @@ -0,0 +1,89 @@ +/* + * 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. + */ + +package main + +import ( + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" + + "github.com/valyala/fastjson" +) + +func main() { + proxywasm.SetVMContext(&vmContext{}) +} + +type vmContext struct { + types.DefaultVMContext +} + +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +type header struct { + Name string + Value string +} + +type pluginContext struct { + types.DefaultPluginContext + Headers []header +} + +func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + data, err := proxywasm.GetPluginConfiguration() + if err != nil { + proxywasm.LogErrorf("error reading plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + var p fastjson.Parser + v, err := p.ParseBytes(data) + if err != nil { + proxywasm.LogErrorf("erorr decoding plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + headers := v.GetArray("headers") + ctx.Headers = make([]header, len(headers)) + for i, hdr := range headers { + ctx.Headers[i] = header{ + Name: string(hdr.GetStringBytes("name")), + Value: string(hdr.GetStringBytes("value")), + } + } + + return types.OnPluginStartStatusOK +} + +func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpContext{parent: ctx} +} + +type httpContext struct { + types.DefaultHttpContext + parent *pluginContext +} + +func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action { + plugin := ctx.parent + for _, hdr := range plugin.Headers { + proxywasm.ReplaceHttpResponseHeader(hdr.Name, hdr.Value) + } + return types.ActionContinue +} From 30c902895b41327907fb007b1ee5e97e6079b6ba Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Fri, 19 Nov 2021 04:10:21 -0600 Subject: [PATCH 109/260] fix: ignore changes of /apisix/plugins/ (#5558) --- apisix/plugin.lua | 16 ++++--- t/cli/test_admin.sh | 111 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index b1b7d660c440..7864a0084ddd 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -275,13 +275,15 @@ function _M.load(config) stream_plugin_names = {} local plugins_conf = config.value -- plugins_conf can be nil when another instance writes into etcd key "/apisix/plugins/" - if plugins_conf then - for _, conf in ipairs(plugins_conf) do - if conf.stream then - core.table.insert(stream_plugin_names, conf.name) - else - core.table.insert(http_plugin_names, conf.name) - end + if not plugins_conf then + return local_plugins + end + + for _, conf in ipairs(plugins_conf) do + if conf.stream then + core.table.insert(stream_plugin_names, conf.name) + else + core.table.insert(http_plugin_names, conf.name) end end end diff --git a/t/cli/test_admin.sh b/t/cli/test_admin.sh index ac691b175f5b..ecfbffcccb2d 100755 --- a/t/cli/test_admin.sh +++ b/t/cli/test_admin.sh @@ -229,3 +229,114 @@ fi make stop echo "pass: sync /apisix/plugins from etcd when disabling admin successfully" + + + +# ignore changes to /apisix/plugins/ due to init_etcd +echo ' +apisix: + enable_admin: false +plugins: + - node-status +nginx_config: + error_log_level: info +' > conf/config.yaml + +rm logs/error.log +make init +make run + +# first time check node status api +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status) +if [ ! $code -eq 200 ]; then + echo "failed: first time check node status api failed" + exit 1 +fi + +# mock another instance init etcd dir +make init +sleep 1 + +# second time check node status api +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status) +if [ ! $code -eq 200 ]; then + echo "failed: second time check node status api failed" + exit 1 +fi + +make stop + +echo "pass: ignore changes to /apisix/plugins/ due to init_etcd successfully" + + +# accept changes to /apisix/plugins when enable_admin is false +echo ' +apisix: + enable_admin: false +plugins: + - node-status +stream_plugins: +' > conf/config.yaml + +rm logs/error.log +make init +make run + +# first time check node status api +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status) +if [ ! $code -eq 200 ]; then + echo "failed: first time check node status api failed" + exit 1 +fi + +sleep 0.5 + +# check http plugins load list +if ! grep -E 'new plugins: {"node-status":true}' logs/error.log; then + echo "failed: first time load http plugins list failed" + exit 1 +fi + +# check stream plugins(no plugins under stream, it will be added below) +if ! grep -E 'failed to read stream plugin list from local file' logs/error.log; then + echo "failed: first time load stream plugins list failed" + exit 1 +fi + +# mock another instance add /apisix/plugins +res=$(etcdctl put "/apisix/plugins" '[{"name":"node-status"},{"name":"example-plugin"},{"stream":true,"name":"mqtt-proxy"}]') +if [[ $res != "OK" ]]; then + echo "failed: failed to set /apisix/plugins to add more plugins" + exit 1 +fi + +sleep 0.5 + +# second time check node status api +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/status) +if [ ! $code -eq 200 ]; then + echo "failed: second time check node status api failed" + exit 1 +fi + +# check http plugins load list +if ! grep -E 'new plugins: {"node-status":true}' logs/error.log; then + echo "failed: second time load http plugins list failed" + exit 1 +fi + +# check stream plugins load list +if ! grep -E 'new plugins: {.*example-plugin' logs/error.log; then + echo "failed: second time load stream plugins list failed" + exit 1 +fi + + +if grep -E 'new plugins: {}' logs/error.log; then + echo "failed: second time load plugins list failed" + exit 1 +fi + +make stop + +echo "pass: ccept changes to /apisix/plugins successfully" From 3fa0c335347a2f980efef94132dfd16c9cce38ea Mon Sep 17 00:00:00 2001 From: Wen Ming Date: Sun, 21 Nov 2021 13:56:46 +0800 Subject: [PATCH 110/260] docs: add Azure serverless functions in README. (#5560) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fea4087812ff..6a96e822f8ae 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - [Hot Updates And Hot Plugins](docs/en/latest/architecture-design/plugin.md): Continuously updates its configurations and plugins without restarts! - [Proxy Rewrite](docs/en/latest/plugins/proxy-rewrite.md): Support rewrite the `host`, `uri`, `schema`, `enable_websocket`, `headers` of the request before send to upstream. - [Response Rewrite](docs/en/latest/plugins/response-rewrite.md): Set customized response status code, body and header to the client. - - [Serverless](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX. - Dynamic Load Balancing: Round-robin load balancing with weight. - Hash-based Load Balancing: Load balance with consistent hashing sessions. - [Health Checks](docs/en/latest/health-check.md): Enable health check on the upstream node and will automatically filter unhealthy nodes during load balancing to ensure system stability. @@ -146,6 +145,10 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - Custom load balancing algorithms: You can use custom load balancing algorithms during the `balancer` phase. - Custom routing: Support users to implement routing algorithms themselves. +- **Serverless** + - [Lua functions](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX. + - [Azure functions](docs/en/latest/plugins/azure-functions.md): seamless integration with Azure Serverless Function as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud. + ## Get Started 1. Installation From 2262e1c93a516be4f48faabacf6aaf28fd49c0dd Mon Sep 17 00:00:00 2001 From: Xunzhuo Date: Sun, 21 Nov 2021 20:07:42 +0800 Subject: [PATCH 111/260] feat(request-validation): add custom rejected_code (#5553) --- apisix/plugins/request-validation.lua | 11 +- docs/en/latest/plugins/request-validation.md | 1 + docs/zh/latest/plugins/request-validation.md | 1 + t/plugin/request-validation.t | 163 +++++++++++++++++++ 4 files changed, 171 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/request-validation.lua b/apisix/plugins/request-validation.lua index bdd26fa84918..8f383bf3051e 100644 --- a/apisix/plugins/request-validation.lua +++ b/apisix/plugins/request-validation.lua @@ -26,6 +26,7 @@ local schema = { properties = { header_schema = {type = "object"}, body_schema = {type = "object"}, + rejected_code = {type = "integer", minimum = 200, maximum = 599}, rejected_msg = {type = "string", minLength = 1, maxLength = 256} }, anyOf = { @@ -75,7 +76,7 @@ function _M.rewrite(conf) local ok, err = core.schema.check(conf.header_schema, headers) if not ok then core.log.error("req schema validation failed", err) - return 400, conf.rejected_msg or err + return conf.rejected_code or 400, conf.rejected_msg or err end end @@ -87,11 +88,11 @@ function _M.rewrite(conf) if not body then local filename = ngx.req.get_body_file() if not filename then - return 500, conf.rejected_msg + return conf.rejected_code or 500, conf.rejected_msg end local fd = io.open(filename, 'rb') if not fd then - return 500, conf.rejected_msg + return conf.rejected_code or 500, conf.rejected_msg end body = fd:read('*a') end @@ -104,13 +105,13 @@ function _M.rewrite(conf) if not req_body then core.log.error('failed to decode the req body', error) - return 400, conf.rejected_msg or error + return conf.rejected_code or 400, conf.rejected_msg or error end local ok, err = core.schema.check(conf.body_schema, req_body) if not ok then core.log.error("req schema validation failed", err) - return 400, conf.rejected_msg or err + return conf.rejected_code or 400, conf.rejected_msg or err end end end diff --git a/docs/en/latest/plugins/request-validation.md b/docs/en/latest/plugins/request-validation.md index d2a17b2ba8c4..42951bddb8da 100644 --- a/docs/en/latest/plugins/request-validation.md +++ b/docs/en/latest/plugins/request-validation.md @@ -45,6 +45,7 @@ For more information on schema, refer to [JSON schema](https://github.com/api7/j | ---------------- | ------ | ----------- | ------- | ----- | -------------------------- | | header_schema | object | optional | | | schema for the header data | | body_schema | object | optional | | | schema for the body data | +| rejected_code | integer | optional | | [200,...,599] | the custom rejected code | | rejected_msg | string | optional | | | the custom rejected message | ## How To Enable diff --git a/docs/zh/latest/plugins/request-validation.md b/docs/zh/latest/plugins/request-validation.md index a1ba46d5ddb1..e9f5ed87cef3 100644 --- a/docs/zh/latest/plugins/request-validation.md +++ b/docs/zh/latest/plugins/request-validation.md @@ -44,6 +44,7 @@ title: request-validation | ---------------- | ------ | ----------- | ------- | ----- | --------------------------------- | | header_schema | object | 可选 | | | `header` 数据的 `schema` 数据结构 | | body_schema | object | 可选 | | | `body` 数据的 `schema` 数据结构 | +| rejected_code | integer | 可选 | | [200,...,599] | 自定义拒绝状态码 | | rejected_msg | string | 可选 | | | 自定义拒绝信息 | ## 如何启用 diff --git a/t/plugin/request-validation.t b/t/plugin/request-validation.t index 711bf22585a4..6e3e697416a4 100644 --- a/t/plugin/request-validation.t +++ b/t/plugin/request-validation.t @@ -1658,3 +1658,166 @@ qr/object matches none of the requireds/ 400 --- no_error_log [error] + + + +=== TEST 45: add route (test request validation `body_schema.required` success with custom reject code) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "request-validation": { + "body_schema": { + "type": "object", + "properties": { + "test": { + "type": "string", + "enum": ["a", "b", "c"] + } + }, + "required": ["test"] + }, + "rejected_code": 505 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]]) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 46: use empty body to hit custom rejected code rule +--- request +GET /opentracing +--- error_code: 505 +--- no_error_log +[error] + + + +=== TEST 47: use bad body value to hit custom rejected code rule +--- request +POST /opentracing +{"test":"abc"} +--- error_code: 505 +--- error_log eval +qr/schema validation failed/ + + + +=== TEST 48: pass custom rejected code rule +--- request +POST /opentracing +{"test":"a"} +--- error_code: 200 +--- response_body eval +qr/opentracing/ +--- no_error_log +[error] + + + +=== TEST 49: add route (test request validation `header_schema.required` failure with custom reject code) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "request-validation": { + "header_schema": { + "type": "object", + "properties": { + "test": { + "type": "string", + "enum": ["a", "b", "c"] + } + }, + "required": ["test"] + }, + "rejected_code": 10000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/plugin/request/validation" + }]]) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body_like eval +qr/expected 10000 to be smaller than 599/ +--- error_code chomp +400 +--- no_error_log +[error] + + + +=== TEST 50: add route (test request validation schema with custom reject code only) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "request-validation": { + "rejected_code": 505 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/plugin/request/validation" + }]]) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body_like eval +qr/object matches none of the requireds/ +--- error_code chomp +400 +--- no_error_log +[error] From 24f813abb03ddd1baa149d1a5a340a2cca6894f7 Mon Sep 17 00:00:00 2001 From: fishcui <2510271615@qq.com> Date: Mon, 22 Nov 2021 09:05:06 +0800 Subject: [PATCH 112/260] docs: update the MAINTAIN.md to use correct website configuration link (#5565) --- MAINTAIN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAIN.md b/MAINTAIN.md index 96de8d5c154f..ed124d87b4ed 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -30,7 +30,7 @@ via `VERSION=x.y.z make release-src` 6. When the vote is passed, send the vote result email to dev@apisix.apache.org 7. Move the vote artifact to Apache's apisix repo 8. Create a GitHub release from the minor branch -9. Update [APISIX's website](https://github.com/apache/apisix-website/blob/master/website/docusaurus.config.js#L110-L123) +9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) 10. Update APISIX docker 11. Update APISIX rpm package 12. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org From 3f043abff627b3009e2c62fa7ba1b7341dab7e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 22 Nov 2021 09:37:58 +0800 Subject: [PATCH 113/260] feat(request-validation): default code is 400 (#5563) --- apisix/plugins/request-validation.lua | 34 +++++++------------- docs/en/latest/plugins/request-validation.md | 2 +- docs/zh/latest/plugins/request-validation.md | 2 +- t/plugin/request-validation.t | 14 +++----- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/apisix/plugins/request-validation.lua b/apisix/plugins/request-validation.lua index 8f383bf3051e..e64a9cec372d 100644 --- a/apisix/plugins/request-validation.lua +++ b/apisix/plugins/request-validation.lua @@ -17,16 +17,13 @@ local core = require("apisix.core") local plugin_name = "request-validation" local ngx = ngx -local io = io -local req_read_body = ngx.req.read_body -local req_get_body_data = ngx.req.get_body_data local schema = { type = "object", properties = { header_schema = {type = "object"}, body_schema = {type = "object"}, - rejected_code = {type = "integer", minimum = 200, maximum = 599}, + rejected_code = {type = "integer", minimum = 200, maximum = 599, default = 400}, rejected_msg = {type = "string", minLength = 1, maxLength = 256} }, anyOf = { @@ -76,42 +73,35 @@ function _M.rewrite(conf) local ok, err = core.schema.check(conf.header_schema, headers) if not ok then core.log.error("req schema validation failed", err) - return conf.rejected_code or 400, conf.rejected_msg or err + return conf.rejected_code, conf.rejected_msg or err end end if conf.body_schema then - req_read_body() - local req_body, error - local body = req_get_body_data() - + local req_body + local body, err = core.request.get_body() if not body then - local filename = ngx.req.get_body_file() - if not filename then - return conf.rejected_code or 500, conf.rejected_msg - end - local fd = io.open(filename, 'rb') - if not fd then - return conf.rejected_code or 500, conf.rejected_msg + if err then + core.log.error("failed to get body: ", err) end - body = fd:read('*a') + return conf.rejected_code, conf.rejected_msg end if headers["content-type"] == "application/x-www-form-urlencoded" then - req_body, error = ngx.decode_args(body) + req_body, err = ngx.decode_args(body) else -- JSON as default - req_body, error = core.json.decode(body) + req_body, err = core.json.decode(body) end if not req_body then - core.log.error('failed to decode the req body', error) - return conf.rejected_code or 400, conf.rejected_msg or error + core.log.error('failed to decode the req body', err) + return conf.rejected_code, conf.rejected_msg or err end local ok, err = core.schema.check(conf.body_schema, req_body) if not ok then core.log.error("req schema validation failed", err) - return conf.rejected_code or 400, conf.rejected_msg or err + return conf.rejected_code, conf.rejected_msg or err end end end diff --git a/docs/en/latest/plugins/request-validation.md b/docs/en/latest/plugins/request-validation.md index 42951bddb8da..32109938aebd 100644 --- a/docs/en/latest/plugins/request-validation.md +++ b/docs/en/latest/plugins/request-validation.md @@ -45,7 +45,7 @@ For more information on schema, refer to [JSON schema](https://github.com/api7/j | ---------------- | ------ | ----------- | ------- | ----- | -------------------------- | | header_schema | object | optional | | | schema for the header data | | body_schema | object | optional | | | schema for the body data | -| rejected_code | integer | optional | | [200,...,599] | the custom rejected code | +| rejected_code | integer | optional | 400 | [200,...,599] | the custom rejected code | | rejected_msg | string | optional | | | the custom rejected message | ## How To Enable diff --git a/docs/zh/latest/plugins/request-validation.md b/docs/zh/latest/plugins/request-validation.md index e9f5ed87cef3..e355877d6256 100644 --- a/docs/zh/latest/plugins/request-validation.md +++ b/docs/zh/latest/plugins/request-validation.md @@ -44,7 +44,7 @@ title: request-validation | ---------------- | ------ | ----------- | ------- | ----- | --------------------------------- | | header_schema | object | 可选 | | | `header` 数据的 `schema` 数据结构 | | body_schema | object | 可选 | | | `body` 数据的 `schema` 数据结构 | -| rejected_code | integer | 可选 | | [200,...,599] | 自定义拒绝状态码 | +| rejected_code | integer | 可选 | 400 | [200,...,599] | 自定义拒绝状态码 | | rejected_msg | string | 可选 | | | 自定义拒绝信息 | ## 如何启用 diff --git a/t/plugin/request-validation.t b/t/plugin/request-validation.t index 6e3e697416a4..2893d085a830 100644 --- a/t/plugin/request-validation.t +++ b/t/plugin/request-validation.t @@ -1543,7 +1543,7 @@ passed === TEST 40: use empty body to hit `body_schema.required with custom reject message` rule --- request GET /opentracing ---- error_code: 500 +--- error_code: 400 --- response_body chomp customize reject message for body_schema.required --- no_error_log @@ -1616,8 +1616,7 @@ qr/opentracing/ GET /t --- response_body_like eval qr/string too long/ ---- error_code chomp -400 +--- error_code: 400 --- no_error_log [error] @@ -1654,8 +1653,7 @@ qr/string too long/ GET /t --- response_body_like eval qr/object matches none of the requireds/ ---- error_code chomp -400 +--- error_code: 400 --- no_error_log [error] @@ -1779,8 +1777,7 @@ qr/opentracing/ GET /t --- response_body_like eval qr/expected 10000 to be smaller than 599/ ---- error_code chomp -400 +--- error_code: 400 --- no_error_log [error] @@ -1817,7 +1814,6 @@ qr/expected 10000 to be smaller than 599/ GET /t --- response_body_like eval qr/object matches none of the requireds/ ---- error_code chomp -400 +--- error_code: 400 --- no_error_log [error] From 826100a45192c53fc7a271e2999925f9875af58d Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 22 Nov 2021 00:13:50 -0600 Subject: [PATCH 114/260] docs: update MAINTAIN.md (#5568) --- MAINTAIN.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/MAINTAIN.md b/MAINTAIN.md index ed124d87b4ed..99ea554d9d4e 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -21,18 +21,18 @@ ### Release patch version -1. Create a pull request (contains the changelog and version change) to master, merge it -2. Create a pull request (contains the backport commits, and the change in step 1) to minor branch +1. Create a [pull request](https://github.com/apache/apisix/commit/d13e7f7f0b3f6001cb634598e533a23658927285) (contains the changelog and version change) to master +2. Create a [pull request](https://github.com/apache/apisix/commit/19587ed9f71dd20c5e8dbdc2f79c8f96296e73e3) (contains the backport commits, and the change in step 1) to minor branch 3. Merge it into minor branch 4. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created via `VERSION=x.y.z make release-src` -5. Send the vote email to dev@apisix.apache.org -6. When the vote is passed, send the vote result email to dev@apisix.apache.org +5. Send the [vote email](https://lists.apache.org/thread/vq4qtwqro5zowpdqhx51oznbjy87w9d0) to dev@apisix.apache.org +6. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/k2frnvj4zj9oynsbr7h7nd6n6m3q5p89) to dev@apisix.apache.org 7. Move the vote artifact to Apache's apisix repo -8. Create a GitHub release from the minor branch +8. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.2) from the minor branch 9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) -10. Update APISIX docker -11. Update APISIX rpm package +10. Update APISIX rpm package +11. Update APISIX docker 12. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org ### Release minor version @@ -46,6 +46,6 @@ via `VERSION=x.y.z make release-src` 6. Create a GitHub release from the minor branch 7. Merge the pull request into master branch 8. Update APISIX website -9. Update APISIX docker -10. Update APISIX rpm package +9. Update APISIX rpm package +10. Update APISIX docker 11. Send the ANNOUNCE email to dev@apisix.apache.org & announce@apache.org From db4ad744ea9fc8cd4706a66e4797874a7e16e015 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Mon, 22 Nov 2021 11:49:25 +0530 Subject: [PATCH 115/260] docs: addition of multi language support into readme (#5543) --- README.md | 6 ++++++ .../images/apisix-multi-lang-support.png | Bin 0 -> 212677 bytes 2 files changed, 6 insertions(+) create mode 100644 docs/assets/images/apisix-multi-lang-support.png diff --git a/README.md b/README.md index 6a96e822f8ae..9e68ea89becd 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,12 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - Custom load balancing algorithms: You can use custom load balancing algorithms during the `balancer` phase. - Custom routing: Support users to implement routing algorithms themselves. +- **Multi-Language support** + - Apache APISIX is a multi-language gateway for plugin development and provides support via `WASM` and `RPC`. + ![Multi Language Support into Apache APISIX](docs/assets/images/apisix-multi-lang-support.png) + - The WASM or WebAssembly, is the modern way. APISIX can load and run WASM bytecode via APISIX [wasm plugin](https://github.com/apache/apisix/blob/master/docs/en/latest/wasm.md) written with the [Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks). Developers only need to write the code according to the SDK and then compile it into a WASM bytecode that runs on WASM VM with APISIX. + - The RPC way, is a traditional way. Developers can choose the language according to their needs and after starting an independent process with the RPC, it exchanges data with APISIX through local RPC communication. Till this moment, APISIX has support for [Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) and Node.js. + - **Serverless** - [Lua functions](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX. - [Azure functions](docs/en/latest/plugins/azure-functions.md): seamless integration with Azure Serverless Function as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud. diff --git a/docs/assets/images/apisix-multi-lang-support.png b/docs/assets/images/apisix-multi-lang-support.png new file mode 100644 index 0000000000000000000000000000000000000000..c3110e4e338782998783770d9f13437f19e71313 GIT binary patch literal 212677 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 UR{p~5*wdtAd$XN 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

XzX?&gM{_$g+`V{c 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

>~LeN>a7_%rRV+9M;%tYm`&N&?Dt_1jp9Fr@ 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 ZZR=wd-qN#NsOABG$B&&jNXD1HC{ literal 0 HcmV?d00001 From 2a32f13824c0cc8e359feedd23e537422af3b28c Mon Sep 17 00:00:00 2001 From: Xunzhuo Date: Tue, 23 Nov 2021 09:05:29 +0800 Subject: [PATCH 116/260] docs(style): fix non-standard spaces between letters (#5566) --- docs/zh/latest/FAQ.md | 20 ++++++------- .../latest/architecture-design/debug-mode.md | 2 +- docs/zh/latest/architecture-design/route.md | 2 +- .../zh/latest/architecture-design/upstream.md | 18 ++++++------ docs/zh/latest/certificate.md | 8 +++--- docs/zh/latest/control-api.md | 2 +- docs/zh/latest/debug-function.md | 4 +-- docs/zh/latest/discovery.md | 2 +- docs/zh/latest/discovery/nacos.md | 2 +- docs/zh/latest/getting-started.md | 16 +++++------ docs/zh/latest/how-to-build.md | 16 +++++------ docs/zh/latest/mtls.md | 2 +- docs/zh/latest/plugin-develop.md | 4 +-- docs/zh/latest/plugins/api-breaker.md | 10 +++---- .../zh/latest/plugins/consumer-restriction.md | 6 ++-- docs/zh/latest/plugins/cors.md | 2 +- docs/zh/latest/plugins/dubbo-proxy.md | 4 +-- docs/zh/latest/plugins/echo.md | 2 +- docs/zh/latest/plugins/error-log-logger.md | 4 +-- docs/zh/latest/plugins/fault-injection.md | 2 +- docs/zh/latest/plugins/hmac-auth.md | 4 +-- docs/zh/latest/plugins/ip-restriction.md | 4 +-- docs/zh/latest/plugins/jwt-auth.md | 4 +-- docs/zh/latest/plugins/kafka-logger.md | 6 ++-- docs/zh/latest/plugins/key-auth.md | 4 +-- docs/zh/latest/plugins/limit-count.md | 6 ++-- docs/zh/latest/plugins/limit-req.md | 20 ++++++------- docs/zh/latest/plugins/mqtt-proxy.md | 2 +- docs/zh/latest/plugins/openid-connect.md | 10 +++---- docs/zh/latest/plugins/prometheus.md | 4 +-- docs/zh/latest/plugins/proxy-cache.md | 12 ++++---- docs/zh/latest/plugins/proxy-mirror.md | 2 +- docs/zh/latest/plugins/proxy-rewrite.md | 6 ++-- docs/zh/latest/plugins/redirect.md | 2 +- docs/zh/latest/plugins/request-id.md | 2 +- docs/zh/latest/plugins/response-rewrite.md | 6 ++-- docs/zh/latest/plugins/skywalking-logger.md | 2 +- docs/zh/latest/plugins/sls-logger.md | 20 ++++++------- docs/zh/latest/plugins/syslog.md | 10 +++---- docs/zh/latest/plugins/tcp-logger.md | 14 +++++----- docs/zh/latest/plugins/traffic-split.md | 28 +++++++++---------- docs/zh/latest/plugins/ua-restriction.md | 4 +-- docs/zh/latest/plugins/udp-logger.md | 2 +- docs/zh/latest/plugins/uri-blocker.md | 10 +++---- docs/zh/latest/plugins/wolf-rbac.md | 2 +- docs/zh/latest/plugins/zipkin.md | 4 +-- docs/zh/latest/profile.md | 6 ++-- docs/zh/latest/router-radixtree.md | 6 ++-- docs/zh/latest/stand-alone.md | 2 +- docs/zh/latest/stream-proxy.md | 4 +-- 50 files changed, 168 insertions(+), 168 deletions(-) diff --git a/docs/zh/latest/FAQ.md b/docs/zh/latest/FAQ.md index 742a215a3a55..2f245e27e364 100644 --- a/docs/zh/latest/FAQ.md +++ b/docs/zh/latest/FAQ.md @@ -27,7 +27,7 @@ title: 常见问题 ## APISIX 和其他的 API 网关有什么不同之处? -APISIX 基于 etcd 来完成配置的保存和同步,而不是 postgres 或者 MySQL 这类关系型数据库。 +APISIX 基于 etcd 来完成配置的保存和同步,而不是 PostgreSQL 或者 MySQL 这类关系型数据库。 这样不仅去掉了轮询,让代码更加的简洁,配置同步也更加实时。同时系统也不会存在单点,可用性更高。 另外,APISIX 具备动态路由和插件热加载,特别适合微服务体系下的 API 管理。 @@ -46,7 +46,7 @@ APISIX 是当前性能最好的 API 网关,单核 QPS 达到 2.3 万,平均 当然可以,APISIX 提供了灵活的自定义插件,方便开发者和企业编写自己的逻辑。 -[如何开发插件](plugin-develop.md) +具体可参考:[如何开发插件](plugin-develop.md) ## 我们为什么选择 etcd 作为配置中心? @@ -170,7 +170,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 }' ``` -3. 使用`serverless`插件: +3. 使用 `serverless` 插件: ```shell curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -213,18 +213,18 @@ Server: APISIX web server ## 如何修改日志等级 -默认的 APISIX 日志等级为`warn`,如果需要查看`core.log.info`的打印结果需要将日志等级调整为`info`。 +默认的 APISIX 日志等级为 `warn`,如果需要查看 `core.log.info` 的打印结果需要将日志等级调整为 `info`。 具体步骤: -1、修改 conf/config.yaml 中的 `nginx_config` 配置参数`error_log_level: "warn"` 为 `error_log_level: "info"`。 +1、修改 conf/config.yaml 中的 `nginx_config` 配置参数 `error_log_level: "warn"` 为 `error_log_level: "info"`。 ```yaml nginx_config: error_log_level: "info" ``` -2、重启抑或 reload APISIX +2、重启或 reload APISIX 之后便可以在 logs/error.log 中查看到 info 的日志了。 @@ -238,7 +238,7 @@ Apache APISIX 的插件支持热加载。 默认情况下,APISIX 在处理 HTTP 请求时只监听 9080 端口。如果你想让 APISIX 监听多个端口,你需要修改配置文件中的相关参数,具体步骤如下: -1. 修改 `conf/config.yaml` 中 HTTP 端口监听的参数`node_listen`,示例: +1. 修改 `conf/config.yaml` 中 HTTP 端口监听的参数 `node_listen`,示例: ``` apisix: @@ -248,7 +248,7 @@ Apache APISIX 的插件支持热加载。 - 9082 ``` - 处理 HTTPS 请求也类似,修改`conf/config.yaml`中 HTTPS 端口监听的参数`ssl.listen_port`,示例: + 处理 HTTPS 请求也类似,修改 `conf/config.yaml` 中 HTTPS 端口监听的参数 `ssl.listen_port`,示例: ``` apisix: @@ -268,7 +268,7 @@ etcd 提供订阅接口用于监听指定关键字、目录是否发生变更( APISIX 主要使用 [etcd.watchdir](https://github.com/api7/lua-resty-etcd/blob/master/api_v3.md#watchdir) 监视目录内容变更: * 如果监听目录没有数据更新:该调用会被阻塞,直到超时或其他错误返回。 -* 如果监听目录有数据更新:etcd 将立刻返回订阅(毫秒级)到的新数据,APISIX 将它更新到内存缓存。 +* 如果监听目录有数据更新:etcd 将立刻返回订阅(毫秒级)到的新数据,APISIX 将它更新到内存缓存。 借助 etcd 增量通知毫秒级特性,APISIX 也就完成了毫秒级的配置同步。 @@ -407,7 +407,7 @@ HTTP/1.1 404 Not Found ## upstream 节点是否支持配置 [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) 地址? -这是支持的,下面是一个 `FQDN` 为 `httpbin.default.svc.cluster.local`(一个 Kubernetes Service) 的示例: +这是支持的,下面是一个 `FQDN` 为 `httpbin.default.svc.cluster.local`(一个 Kubernetes Service) 的示例: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/docs/zh/latest/architecture-design/debug-mode.md b/docs/zh/latest/architecture-design/debug-mode.md index a4e7692d306a..a335064472a3 100644 --- a/docs/zh/latest/architecture-design/debug-mode.md +++ b/docs/zh/latest/architecture-design/debug-mode.md @@ -32,7 +32,7 @@ basic: 注意:在 APISIX 2.10 之前,开启基本调试模式曾经是设置 `conf/config.yaml` 中的 `apisix.enable_debug` 为 `true`。 -比如对 `/hello` 开启了 `limit-conn`和`limit-count`插件,这时候应答头中会有 `Apisix-Plugins: limit-conn, limit-count`。 +比如对 `/hello` 开启了 `limit-conn` 和 `limit-count` 插件,这时候应答头中会有 `Apisix-Plugins: limit-conn, limit-count`。 ```shell $ curl http://127.0.0.1:1984/hello -i diff --git a/docs/zh/latest/architecture-design/route.md b/docs/zh/latest/architecture-design/route.md index 18ddc9ec73ab..32e85bc1f873 100644 --- a/docs/zh/latest/architecture-design/route.md +++ b/docs/zh/latest/architecture-design/route.md @@ -24,7 +24,7 @@ title: Route Route 字面意思就是路由,通过定义一些规则来匹配客户端的请求,然后根据匹配结果加载并执行相应的 插件,并把请求转发给到指定 Upstream。 -Route 中主要包含三部分内容:匹配规则(比如 uri、host、remote_addr 等),插件配置(限流限速等)和上游信息。 +Route 中主要包含三部分内容:匹配规则(比如 uri、host、remote_addr 等),插件配(限流限速等)和上游信息。 请看下图示例,是一些 Route 规则的实例,当某些属性值相同时,图中用相同颜色标识。 ![路由示例](../../../assets/images/routes-example.png) diff --git a/docs/zh/latest/architecture-design/upstream.md b/docs/zh/latest/architecture-design/upstream.md index 3941410a93e3..bf97b8ecbdd1 100644 --- a/docs/zh/latest/architecture-design/upstream.md +++ b/docs/zh/latest/architecture-design/upstream.md @@ -32,7 +32,7 @@ Upstream 的配置可以被直接绑定在指定 `Route` 中,也可以被绑 ### 配置参数 -APISIX 的 Upstream 除了基本的负载均衡算法选择外,还支持对上游做主被动健康检查、重试等逻辑,具体看这个[链接](../admin-api.md#upstream)。 +APISIX 的 Upstream 除了基本的负载均衡算法选择外,还支持对上游做主被动健康检查、重试等逻辑,具体看这个 [链接](../admin-api.md#upstream)。 创建上游对象用例: @@ -119,9 +119,9 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` -更多细节可以参考[健康检查的文档](../health-check.md)。 +更多细节可以参考 [健康检查的文档](../health-check.md)。 -下面是几个使用不同`hash_on`类型的配置示例: +下面是几个使用不同 `hash_on` 类型的配置示例: #### Consumer @@ -139,7 +139,7 @@ curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f1 }' ``` -新建路由,打开`key-auth`插件认证,`upstream`的`hash_on`类型为`consumer`: +新建路由,打开 `key-auth` 插件认证,`upstream` 的 `hash_on` 类型为 `consumer`: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -159,7 +159,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` -测试请求,认证通过后的`consumer_name`将作为负载均衡哈希算法的哈希值: +测试请求,认证通过后的 `consumer_name` 将作为负载均衡哈希算法的哈希值: ```shell curl http://127.0.0.1:9080/server_port -H "apikey: auth-jack" @@ -167,7 +167,7 @@ curl http://127.0.0.1:9080/server_port -H "apikey: auth-jack" ##### Cookie -新建路由和`Upstream`,`hash_on`类型为`cookie`: +新建路由和 `Upstream`,`hash_on` 类型为 `cookie`: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -185,7 +185,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` -客户端请求携带`Cookie`: +客户端请求携带 `Cookie`: ```shell curl http://127.0.0.1:9080/hash_on_cookie -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -H "Cookie: sid=3c183a30cffcda1408daf1c61d47b274" @@ -193,7 +193,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 ##### Header -新建路由和`Upstream`,`hash_on`类型为`header`, `key`为`content-type`: +新建路由和 `Upstream`,`hash_on` 类型为 `header`,`key` 为 `content-type`: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -211,7 +211,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` -客户端请求携带`content-type`的`header`: +客户端请求携带 `content-type` 的 `header`: ```shell curl http://127.0.0.1:9080/hash_on_header -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -H "Content-Type: application/json" diff --git a/docs/zh/latest/certificate.md b/docs/zh/latest/certificate.md index 85bb53c666dd..d84db1b9414a 100644 --- a/docs/zh/latest/certificate.md +++ b/docs/zh/latest/certificate.md @@ -23,7 +23,7 @@ title: 证书 `APISIX` 支持通过 TLS 扩展 SNI 实现加载特定的 SSL 证书以实现对 https 的支持。 -SNI(Server Name Indication)是用来改善 SSL 和 TLS 的一项特性,它允许客户端在服务器端向其发送证书之前向服务器端发送请求的域名,服务器端根据客户端请求的域名选择合适的SSL证书发送给客户端。 +SNI(Server Name Indication)是用来改善 SSL 和 TLS 的一项特性,它允许客户端在服务器端向其发送证书之前向服务器端发送请求的域名,服务器端根据客户端请求的域名选择合适的 SSL 证书发送给客户端。 ### 单一域名指定 @@ -105,8 +105,8 @@ curl --resolve 'test.com:9443:127.0.0.1' https://test.com:9443/hello -vvv ### 泛域名 -一个 SSL 证书的域名也可能包含泛域名,如`*.test.com`,它代表所有以`test.com`结尾的域名都可以使用该证书。 -比如`*.test.com`,可以匹配 `www.test.com`、`mail.test.com`。 +一个 SSL 证书的域名也可能包含泛域名,如 `*.test.com`,它代表所有以 `test.com` 结尾的域名都可以使用该证书。 +比如 `*.test.com`,可以匹配 `www.test.com`、`mail.test.com`。 看下面这个例子,请注意我们把 `*.test.com` 作为 sni 传递进来: @@ -150,7 +150,7 @@ curl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/hello -v ### 多域名的情况 -如果一个 SSL 证书包含多个独立域名,比如`www.test.com`和`mail.test.com`, +如果一个 SSL 证书包含多个独立域名,比如 `www.test.com` 和 `mail.test.com`, 你可以把它们都放入 `snis` 数组中,就像这样: ```json diff --git a/docs/zh/latest/control-api.md b/docs/zh/latest/control-api.md index acd2b02c71dd..5791b11e0e3a 100644 --- a/docs/zh/latest/control-api.md +++ b/docs/zh/latest/control-api.md @@ -154,7 +154,7 @@ APISIX 中一些插件添加了自己的 control API。如果你对他们感兴 每个 entry 包含以下字段: * src_type:表示 health checker 的来源。值是 `[routes,services,upstreams]` 其中之一 -* src_id:表示创建 health checker 的对象的id。例如,假设 id 为 1 的 Upstream 对象创建了一个 health checker,那么 `src_type` 就是 `upstreams`,`src_id` 就是 1 +* src_id:表示创建 health checker 的对象的 id。例如,假设 id 为 1 的 Upstream 对象创建了一个 health checker,那么 `src_type` 就是 `upstreams`,`src_id` 就是 1 * name: 表示 health checker 的名称 * nodes: health checker 的目标节点 * healthy_nodes: 表示 health checker 检测到的健康节点 diff --git a/docs/zh/latest/debug-function.md b/docs/zh/latest/debug-function.md index 539e67c20336..e214ebe49251 100644 --- a/docs/zh/latest/debug-function.md +++ b/docs/zh/latest/debug-function.md @@ -23,7 +23,7 @@ title: 调试功能 ## `5xx` 响应状态码 -500、502、503等类似的 `5xx` 状态码,是由于服务器错误而响应的状态码,当一个请求出现 `5xx` 状态码时;它可能来源于 `APISIX` 或 `Upstream` 。如何识别这些响应状态码的来源,是一件很有意义的事,它能够快速的帮助我们确定问题的所在。 +500、502、503 等类似的 `5xx` 状态码,是由于服务器错误而响应的状态码,当一个请求出现 `5xx` 状态码时;它可能来源于 `APISIX` 或 `Upstream` 。如何识别这些响应状态码的来源,是一件很有意义的事,它能够快速的帮助我们确定问题的所在。 ## 如何识别 `5xx` 响应状态码的来源 @@ -31,7 +31,7 @@ title: 调试功能 ## 示例 -示例1:`502` 响应状态码来源于 `Upstream` (IP地址不可用) +示例1:`502` 响应状态码来源于 `Upstream` (IP 地址不可用) ```shell $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/docs/zh/latest/discovery.md b/docs/zh/latest/discovery.md index 0466cb8bd6da..c5cfda7c765e 100644 --- a/docs/zh/latest/discovery.md +++ b/docs/zh/latest/discovery.md @@ -206,7 +206,7 @@ discovery: ## upstream 配置 -APISIX 是通过 `upstream.discovery_type`选择使用的服务发现, `upstream.service_name` 与注册中心的服务名进行关联。下面是将 URL 为 "/user/\*" 的请求路由到注册中心名为 "USER-SERVICE" 的服务上例子: +APISIX 是通过 `upstream.discovery_type` 选择使用的服务发现,`upstream.service_name` 与注册中心的服务名进行关联。下面是将 URL 为 "/user/\*" 的请求路由到注册中心名为 "USER-SERVICE" 的服务上例子: ```shell $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' diff --git a/docs/zh/latest/discovery/nacos.md b/docs/zh/latest/discovery/nacos.md index ddec79089238..a251e82559db 100644 --- a/docs/zh/latest/discovery/nacos.md +++ b/docs/zh/latest/discovery/nacos.md @@ -31,7 +31,7 @@ Nacos 服务发现模块目前是实验性的。 ### Nacos 配置 -在文件 `conf/config.yaml` 中添加以下配置到: +在文件 `conf/config.yaml` 中添加以下配置到: ```yaml discovery: diff --git a/docs/zh/latest/getting-started.md b/docs/zh/latest/getting-started.md index 4cc042a87f1e..b7c996d0927e 100644 --- a/docs/zh/latest/getting-started.md +++ b/docs/zh/latest/getting-started.md @@ -40,8 +40,8 @@ title: 快速入门指南 - Protocol:即网络传输协议,示例中使用的是最常见的 `HTTP` 协议。 - Port:即端口,示例中使用的 `80` 端口。 - Host:即宿主机,示例中的主机是 `httpbin.org`。 -- Path:即路径,示例中的路径是`/get`。 -- Query Parameters:即查询字符串,这里有两个字符串,分别是`foo1`和`foo2`。 +- Path:即路径,示例中的路径是 `/get`。 +- Query Parameters:即查询字符串,这里有两个字符串,分别是 `foo1` 和 `foo2`。 运行以下命令,发送请求: @@ -70,9 +70,9 @@ curl --location --request GET "http://httpbin.org/get?foo1=bar1&foo2=bar2" ## 前提条件 -- 已安装[Docker Compose 组件](https://docs.docker.com/compose/)。 +- 已安装 [Docker Compose 组件](https://docs.docker.com/compose/)。 -- 本文使用 [curl](https://curl.se/docs/manpage.html) 命令行进行 API 测试。您也可以使用其他工具例如 [Postman](https://www.postman.com/)等,进行测试。 +- 本文使用 [curl](https://curl.se/docs/manpage.html) 命令行进行 API 测试。您也可以使用其他工具例如 [Postman](https://www.postman.com/) 等,进行测试。 + +## Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +`google-cloud-logging` plugin is used to send the access log of `Apache APISIX` to the [Google Cloud Logging Service](https://cloud.google.com/logging/). + +This plugin provides the ability to push log data as a batch to Google Cloud logging Service. + +For more info on Batch-Processor in Apache APISIX please refer: +[Batch-Processor](../batch-processor.md) + +## Attributes + +| Name | Requirement | Default | Description | +| ----------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| auth_config | Semi-optional | | one of `auth_config` or `auth_file` must be configured | +| auth_config.private_key | required | | the private key parameters of the Google service account | +| auth_config.project_id | required | | the project id parameters of the Google service account | +| auth_config.token_uri | optional | https://oauth2.googleapis.com/token | the token uri parameters of the Google service account | +| auth_config.entries_uri | optional | https://logging.googleapis.com/v2/entries:write | google cloud logging service API | +| auth_config.scopes | optional | ["https://www.googleapis.com/auth/logging.read","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/logging.admin","https://www.googleapis.com/auth/cloud-platform"] | the access scopes parameters of the Google service account, refer to: [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#logging) | +| auth_config.ssl_verify | optional | true | enable `SSL` verification, option as per [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) | +| auth_file | semi-optional | | path to the google service account json file(Semi-optional, one of auth_config or auth_file must be configured) | +| resource | optional | {"type": "global"} | the Google monitor resource, refer to: [MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource) | +| log_id | optional | apisix.apache.org%2Flogs | google cloud logging id, refer to: [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) | +| max_retry_count | optional | 0 | max number of retries before removing from the processing pipe line | +| retry_delay | optional | 1 | number of seconds the process execution should be delayed if the execution fails | +| buffer_duration | optional | 60 | max age in seconds of the oldest entry in a batch before the batch must be processed | +| inactive_timeout | optional | 10 | max age in seconds when the buffer will be flushed if inactive | +| batch_max_size | optional | 100 | max size of each batch | + +## How To Enable + +The following is an example of how to enable the `google-cloud-logging` for a specific route. + +### Full configuration + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "google-cloud-logging": { + "auth_config":{ + "project_id":"apisix", + "private_key":"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----", + "token_uri":"https://oauth2.googleapis.com/token", + "scopes":[ + "https://www.googleapis.com/auth/logging.admin" + ], + "entries_uri":"https://logging.googleapis.com/v2/entries:write" + }, + "resource":{ + "type":"global" + }, + "log_id":"apisix.apache.org%2Flogs", + "inactive_timeout":10, + "max_retry_count":0, + "buffer_duration":60, + "retry_delay":1, + "batch_max_size":1 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +### Minimize configuration + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "google-cloud-logging": { + "auth_config":{ + "project_id":"apisix", + "private_key":"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----" + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +## Test Plugin + +* Success + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +... +hello, world +``` + +* Login to Google Cloud to view logging service + +[Google Cloud Logging Service](https://console.cloud.google.com/logs/viewer) + +## Disable Plugin + +Disabling the `google-cloud-logging` plugin is very simple, just remove the `JSON` configuration corresponding to `google-cloud-logging`. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 58cec16b50fe..7e4138874650 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -118,7 +118,8 @@ "plugins/syslog", "plugins/log-rotate", "plugins/error-log-logger", - "plugins/sls-logger" + "plugins/sls-logger", + "plugins/google-cloud-logging" ] }, { diff --git a/docs/zh/latest/plugins/google-cloud-logging.md b/docs/zh/latest/plugins/google-cloud-logging.md new file mode 100644 index 000000000000..18503d1184f6 --- /dev/null +++ b/docs/zh/latest/plugins/google-cloud-logging.md @@ -0,0 +1,157 @@ +--- +title: google-cloud-logging +--- + + + +## 摘要 + +- [**定义**](#定义) +- [**属性列表**](#属性列表) +- [**如何开启**](#如何开启) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 定义 + +`google-cloud-logging` 插件用于将 `Apache APISIX` 的请求日志发送到 [Google Cloud Logging Service](https://cloud.google.com/logging/)。 + +该插件提供了将请求的日志数据以批处理队列的形式推送到谷歌云日志服务的功能。 + +有关 `Apache APISIX` 的 `Batch-Processor` 的更多信息,请参考: +[Batch-Processor](../batch-processor.md) + +## 属性列表 + +| 名称 | 是否必需 | 默认值 | 描述 | +| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| auth_config | 半可选 | | 必须配置 `auth_config` 或 `auth_file` 之一 | +| auth_config.private_key | 必选 | | 谷歌服务帐号的私钥参数 | +| auth_config.project_id | 必选 | | 谷歌服务帐号的项目ID | +| auth_config.token_uri | 可选 | https://oauth2.googleapis.com/token | 请求谷歌服务帐户的令牌的URI | +| auth_config.entries_uri | 可选 | https://logging.googleapis.com/v2/entries:write | 谷歌日志服务写入日志条目的API | +| auth_config.scopes | 可选 | ["https://www.googleapis.com/auth/logging.read","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/logging.admin","https://www.googleapis.com/auth/cloud-platform"] | 谷歌服务账号的访问范围, 参考: [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#logging) | +| auth_config.ssl_verify | 可选 | true | 启用 `SSL` 验证, 配置根据 [OpenResty文档](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) 选项| +| auth_file | 半可选 | | 谷歌服务账号JSON文件的路径(必须配置 `auth_config` 或 `auth_file` 之一) | +| resource | 可选 | {"type": "global"} | 谷歌监控资源,参考: [MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource) | +| log_id | 可选 | apisix.apache.org%2Flogs | 谷歌日志ID,参考: [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) | +| max_retry_count | 可选 | 0 | 从处理管道中移除之前的最大重试次数 | +| retry_delay | 可选 | 1 | 如果执行失败,流程执行应延迟的秒数 | +| buffer_duration | 可选 | 60 | 必须先处理批次中最旧条目的最大期限(以秒为单位) | +| inactive_timeout | 可选 | 10 | 刷新缓冲区的最大时间(以秒为单位) | +| batch_max_size | 可选 | 100 | 每个批处理队列可容纳的最大条目数 | + +## 如何开启 + +1. 下面例子展示了如何为指定路由开启 `google-cloud-logging` 插件。 + +### 完整配置 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "google-cloud-logging": { + "auth_config":{ + "project_id":"apisix", + "private_key":"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----", + "token_uri":"https://oauth2.googleapis.com/token", + "scopes":[ + "https://www.googleapis.com/auth/logging.admin" + ], + "entries_uri":"https://logging.googleapis.com/v2/entries:write" + }, + "resource":{ + "type":"global" + }, + "log_id":"apisix.apache.org%2Flogs", + "inactive_timeout":10, + "max_retry_count":0, + "max_retry_count":0, + "buffer_duration":60, + "retry_delay":1, + "batch_max_size":1 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +### 最小化配置 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "google-cloud-logging": { + "auth_config":{ + "project_id":"apisix", + "private_key":"-----BEGIN RSA PRIVATE KEY-----your private key-----END RSA PRIVATE KEY-----" + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` + +## 测试插件 + +* 成功的情况 + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +... +hello, world +``` + +* 登录谷歌云日志服务,查看日志 + +[Google Cloud Logging Service](https://console.cloud.google.com/logs/viewer) + +## 禁用插件 + +禁用 `google-cloud-logging` 插件非常简单,只需将 `google-cloud-logging` 对应的 `JSON` 配置移除即可。 + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 4821717a6a94..2c62bcafc9d7 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -40,7 +40,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","azure-functions","serverless-post-function","ext-plugin-post-req"\]/ +qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","google-cloud-logging","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","azure-functions","serverless-post-function","ext-plugin-post-req"\]/ --- no_error_log [error] diff --git a/t/lib/server.lua b/t/lib/server.lua index 02550d68ec99..39320abe5b90 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -17,6 +17,23 @@ local json_decode = require("toolkit.json").decode local json_encode = require("toolkit.json").encode +local rsa_public_key = [[ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr +7noq/0ukiZqVQLSJPMOv0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQ== +-----END PUBLIC KEY-----]] + +local rsa_private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv +0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7 ++pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL +wQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF +IeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb +2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs +YvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG +-----END RSA PRIVATE KEY-----]] + local _M = {} @@ -426,6 +443,82 @@ function _M._well_known_openid_configuration() ngx.say(openid_data) end +function _M.google_logging_token() + ngx.req.read_body() + local data = ngx.decode_args(ngx.req.get_body_data()) + local jwt = require("resty.jwt") + local access_scopes = "https://apisix.apache.org/logs:admin" + local verify = jwt:verify(rsa_public_key, data["assertion"]) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scopes" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + expire_time = 0 + end + + local jwt_token = jwt:sign(rsa_private_key, { + header = { typ = "JWT", alg = "RS256" }, + payload = { exp = verify.payload.exp, scope = access_scopes } + }) + + ngx.say(json_encode({ + access_token = jwt_token, + expires_in = expire_time, + token_type = "Bearer" + })) +end + +function _M.google_logging_entries() + ngx.req.read_body() + local data = ngx.req.get_body_data() + local jwt = require("resty.jwt") + local access_scopes = "https://apisix.apache.org/logs:admin" + + local headers = ngx.req.get_headers() + local token = headers["Authorization"] + if not token then + ngx.status = 401 + ngx.say(json_encode({ error = "authentication header not exists" })) + return + end + + token = string.sub(token, string.len("Bearer") + 2) + local verify = jwt:verify(rsa_public_key, token) + if not verify.verified then + ngx.status = 401 + ngx.say(json_encode({ error = "identity authentication failed" })) + return + end + + local scopes_valid = type(verify.payload.scope) == "string" and + verify.payload.scope:find(access_scopes) + if not scopes_valid then + ngx.status = 403 + ngx.say(json_encode({ error = "no access to this scopes" })) + return + end + + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + if expire_time <= 0 then + ngx.status = 403 + ngx.say(json_encode({ error = "token has expired" })) + return + end + + ngx.say(data) +end -- Please add your fake upstream above function _M.go() diff --git a/t/plugin/google-cloud-logging.t b/t/plugin/google-cloud-logging.t new file mode 100644 index 000000000000..db1f2deb62cf --- /dev/null +++ b/t/plugin/google-cloud-logging.t @@ -0,0 +1,378 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + +}); + +run_tests(); + +__DATA__ + +=== TEST 1: Full configuration verification (Auth File) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.google-cloud-logging") + local ok, err = plugin.check_schema({ + auth_file = "/path/to/apache/apisix/auth.json", + resource = { + type = "global" + }, + scopes = { + "https://www.googleapis.com/auth/logging.admin" + }, + log_id = "syslog", + max_retry_count = 0, + retry_delay = 1, + buffer_duration = 60, + inactive_timeout = 10, + batch_max_size = 100, + }) + + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body +passed + + + +=== TEST 2: Full configuration verification (Auth Config) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.google-cloud-logging") + local ok, err = plugin.check_schema({ + auth_config = { + private_key = "private_key", + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/token", + }, + resource = { + type = "global" + }, + scopes = { + "https://www.googleapis.com/auth/logging.admin" + }, + log_id = "syslog", + max_retry_count = 0, + retry_delay = 1, + buffer_duration = 60, + inactive_timeout = 10, + batch_max_size = 100, + }) + + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body +passed + + + +=== TEST 3: Basic configuration verification (Auth File) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.google-cloud-logging") + local ok, err = plugin.check_schema({ + auth_file = "/path/to/apache/apisix/auth.json", + }) + + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body +passed + + + +=== TEST 4: Basic configuration verification (Auth Config) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.google-cloud-logging") + local ok, err = plugin.check_schema({ + auth_config = { + private_key = "private_key", + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/token", + }, + }) + + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body +passed + + + +=== TEST 5: auth configure undefined +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.google-cloud-logging") + local ok, err = plugin.check_schema({ + log_id = "syslog", + max_retry_count = 0, + retry_delay = 1, + buffer_duration = 60, + inactive_timeout = 10, + batch_max_size = 100, + }) + if not ok then + ngx.say(err) + else + ngx.say("passed") + end + } + } +--- response_body +value should match only one schema, but matches none + + + +=== TEST 6: set route (identity authentication failed) +--- config + location /t { + content_by_lua_block { + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_config = { + private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAKeXgPvU/dAfVhOPk5BTBXCaOXy/0S3mY9VHyqvWZBJ97g6tGbLZ +psn6Gw0wC4mxDfEY5ER4YwU1NWCVtIr1XxcCAwEAAQJADkoowVBD4/8IA9r2JhQu +Ho/H3w8r8tH2KTVZ3pUFK15WGJf8vCF9LznVNKCP0X1NMLGvf4yRELx8jjpwJztI +gQIhANdWaJ3AGftJNaF5qXWwniFP1BcyCPSzn3q0rn19NhyHAiEAxz0HN8Yd+7vR +pi0w/L2I/2nLqgPFtqSGpL2KkJYcXPECIQCdM/PD1k4haNzCOXNA++M1JnYLSPfI +zKkMh4MrEZHDWQIhAKasRiKBaUnTCIJ04bs9L6NDtO4Ic9jj8ANW0Nk9yoJxAiAA +tBXLQH7fw5H8RaxBN91yQUZombw6JnRBXKKohWHZ3Q== +-----END RSA PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/logging/token", + scopes = { + "https://apisix.apache.org/logs:admin" + }, + entries_uri = "http://127.0.0.1:1980/google/logging/entries", + }, + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 7: test route (identity authentication failed) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world +--- grep_error_log eval +qr/\{\"error\"\:\"[\w+\s+]*\"\}/ +--- grep_error_log_out +{"error":"identity authentication failed"} +--- error_log +Batch Processor[google-cloud-logging] failed to process entries +Batch Processor[google-cloud-logging] exceeded the max_retry_count + + + +=== TEST 8: set route (no access to this scopes) +--- config + location /t { + content_by_lua_block { + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_config = { + private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv +0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7 ++pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL +wQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF +IeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb +2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs +YvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG +-----END RSA PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/logging/token", + entries_uri = "http://127.0.0.1:1980/google/logging/entries", + }, + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: test route (no access to this scopes) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world +--- grep_error_log eval +qr/\{\"error\"\:\"[\w+\s+]*\"\}/ +--- grep_error_log_out +{"error":"no access to this scopes"} +--- error_log +Batch Processor[google-cloud-logging] failed to process entries +Batch Processor[google-cloud-logging] exceeded the max_retry_count + + + +=== TEST 10: set route (succeed write) +--- config + location /t { + content_by_lua_block { + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_config = { + private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv +0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7 ++pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL +wQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF +IeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb +2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs +YvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG +-----END RSA PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/logging/token", + scopes = { + "https://apisix.apache.org/logs:admin" + }, + entries_uri = "http://127.0.0.1:1980/google/logging/entries", + }, + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 11: test route(succeed write) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world From a163bd6c95f48f0068ae2e07955cb50b1892a718 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Nov 2021 17:30:14 +0800 Subject: [PATCH 118/260] chore(deps): bump actions/cache from 2.1.6 to 2.1.7 (#5577) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/chaos.yml | 2 +- .github/workflows/cli.yml | 2 +- .github/workflows/fuzzing-ci.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6a6a092e7d0..0ac702543a56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: go-version: "1.15" - name: Cache deps - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 env: cache-name: cache-deps with: diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 3122de996134..470b548fd7a9 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -25,7 +25,7 @@ jobs: with: go-version: "1.16" - - uses: actions/cache@v2 + - uses: actions/cache@v2.1.7 with: path: | ~/.cache/go-build diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index e8d20300b5da..bb9832bfb8ae 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -41,7 +41,7 @@ jobs: submodules: recursive - name: Cache deps - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 env: cache-name: cache-deps with: diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index 7fbc26552ccf..b23ec2d4133b 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -29,7 +29,7 @@ jobs: submodules: recursive - name: Cache deps - uses: actions/cache@v2.1.6 + uses: actions/cache@v2.1.7 env: cache-name: cache-deps with: From 1e95f8b70990dd497c7f1c1e130fcfd9f369974a Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Tue, 23 Nov 2021 04:03:22 -0600 Subject: [PATCH 119/260] docs: update MAINTAIN.md for update APISIX docker (#5582) --- MAINTAIN.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MAINTAIN.md b/MAINTAIN.md index 99ea554d9d4e..11f1a238e38c 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -32,20 +32,20 @@ via `VERSION=x.y.z make release-src` 8. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.2) from the minor branch 9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) 10. Update APISIX rpm package -11. Update APISIX docker +11. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` 12. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org ### Release minor version -1. Create a minor branch, and create pull request to master branch from it +1. Create a minor branch, and create [pull request](https://github.com/apache/apisix/commit/bc6ddf51f15e41fffea6c5bd7d01da9838142b66) to master branch from it 2. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created via `VERSION=x.y.z make release-src` -3. Send the vote email to dev@apisix.apache.org -4. When the vote is passed, send the vote result email to dev@apisix.apache.org +3. Send the [vote email](https://lists.apache.org/thread/q8zq276o20r5r9qjkg074nfzb77xwry9) to dev@apisix.apache.org +4. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/p1m9s116rojlhb91g38cj8646393qkz7) to dev@apisix.apache.org 5. Move the vote artifact to Apache's apisix repo -6. Create a GitHub release from the minor branch +6. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.0) from the minor branch 7. Merge the pull request into master branch -8. Update APISIX website +8. Update [APISIX's website](https://github.com/apache/apisix-website/commit/7bf0ab5a1bbd795e6571c4bb89a6e646115e7ca3) 9. Update APISIX rpm package -10. Update APISIX docker -11. Send the ANNOUNCE email to dev@apisix.apache.org & announce@apache.org +10. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` +11. Send the [ANNOUNCE email](https://lists.apache.org/thread/4s4msqwl1tq13p9dnv3hx7skbgpkozw1) to dev@apisix.apache.org & announce@apache.org From 3422713b9cbbd106061b3dc584493c6c0e066e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 23 Nov 2021 21:08:33 +0800 Subject: [PATCH 120/260] ci: fix the broken VERSION env (#5586) --- .github/workflows/build.yml | 2 +- Makefile | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ac702543a56..5de3073e6984 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: - name: Create tarball if: ${{ startsWith(github.ref, 'refs/heads/release/') }} run: | - make compress-tar project_version=${{ steps.branch_env.outputs.version }} + make compress-tar VERSION=${{ steps.branch_env.outputs.version }} - name: Remove source code if: ${{ startsWith(github.ref, 'refs/heads/release/') }} diff --git a/Makefile b/Makefile index 9840514e8cf4..cc83e7eae1d4 100644 --- a/Makefile +++ b/Makefile @@ -22,10 +22,10 @@ SHELL := /bin/bash -o pipefail # Project basic setting +VERSION ?= master project_name ?= apache-apisix -project_version ?= master project_compose_ci ?= ci/pod/docker-compose.yml -project_release_name ?= $(project_name)-$(project_version)-src +project_release_name ?= $(project_name)-$(VERSION)-src # Hyperconverged Infrastructure @@ -368,11 +368,13 @@ release-src: compress-tar .PHONY: compress-tar compress-tar: + # The $VERSION can be major.minor.patch (from developer) + # or major.minor (from the branch name in the CI) $(ENV_TAR) -zcvf $(project_release_name).tgz \ ./apisix \ ./bin \ ./conf \ - ./rockspec/apisix-$(project_version)-*.rockspec \ + ./rockspec/apisix-$(VERSION)*.rockspec \ ./rockspec/apisix-master-0.rockspec \ LICENSE \ Makefile \ From e567937b22a761e52b847cffc3c6d2435bf0b8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 24 Nov 2021 09:08:05 +0800 Subject: [PATCH 121/260] docs(skywalking-logger): fix typo (#5580) --- docs/en/latest/plugins/skywalking-logger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/latest/plugins/skywalking-logger.md b/docs/en/latest/plugins/skywalking-logger.md index 50f365910eab..145c071e48ec 100644 --- a/docs/en/latest/plugins/skywalking-logger.md +++ b/docs/en/latest/plugins/skywalking-logger.md @@ -60,7 +60,7 @@ The following is an example of how to enable the `skywalking-logger` for a speci curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { - "http-logger": { + "skywalking-logger": { "endpoint_addr": "http://127.0.0.1:12800" } }, From 54a0e115e5d639406ddffa9b32edca31f3000733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Wed, 24 Nov 2021 10:57:07 +0800 Subject: [PATCH 122/260] fix: invalid error after passive health check is changed (#5589) --- apisix/schema_def.lua | 22 +++++-- docs/en/latest/health-check.md | 8 +-- docs/zh/latest/health-check.md | 8 +-- t/admin/health-check.t | 114 ++++++++++++++++++--------------- 4 files changed, 87 insertions(+), 65 deletions(-) diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index adddf155d8bf..62b0836a6264 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -237,7 +237,7 @@ local health_checker = { }, successes = { type = "integer", - minimum = 1, + minimum = 0, maximum = 254, default = 5 } @@ -259,24 +259,38 @@ local health_checker = { }, tcp_failures = { type = "integer", - minimum = 1, + minimum = 0, maximum = 254, default = 2 }, timeouts = { type = "integer", - minimum = 1, + minimum = 0, maximum = 254, default = 7 }, http_failures = { type = "integer", - minimum = 1, + minimum = 0, maximum = 254, default = 5 }, } } + }, + default = { + type = "http", + healthy = { + http_statuses = { 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, + 300, 301, 302, 303, 304, 305, 306, 307, 308 }, + successes = 0, + }, + unhealthy = { + http_statuses = { 429, 500, 503 }, + tcp_failures = 0, + timeouts = 0, + http_failures = 0, + }, } } }, diff --git a/docs/en/latest/health-check.md b/docs/en/latest/health-check.md index dc53096ec31b..850a96e60e2d 100644 --- a/docs/en/latest/health-check.md +++ b/docs/en/latest/health-check.md @@ -55,11 +55,11 @@ it whether this unique node is healthy or not. | upstream.checks.active.unhealthy.tcp_failures | Active check (unhealthy node) | integer | `1` to `254` | 2 | Active check (unhealthy node) TCP type check, determine the number of times that the node is not healthy. | | upstream.checks.active.unhealthy.timeouts | Active check (unhealthy node) | integer | `1` to `254` | 3 | Active check (unhealthy node) to determine the number of timeouts for unhealthy nodes. | | upstream.checks.passive.healthy.http_statuses | Passive check (healthy node) | array | `200` to `599` | [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] | Passive check (healthy node) HTTP or HTTPS type check, the HTTP status code of the healthy node. | -| upstream.checks.passive.healthy.successes | Passive check (healthy node) | integer | `1` to `254` | 5 | Passive checks (healthy node) determine the number of times a node is healthy. | +| upstream.checks.passive.healthy.successes | Passive check (healthy node) | integer | `0` to `254` | 5 | Passive checks (healthy node) determine the number of times a node is healthy. | | upstream.checks.passive.unhealthy.http_statuses | Passive check (unhealthy node) | array | `200` to `599` | [429, 500, 503] | Passive check (unhealthy node) HTTP or HTTPS type check, the HTTP status code of the non-healthy node. | -| upstream.checks.passive.unhealthy.tcp_failures | Passive check (unhealthy node) | integer | `1` to `254` | 2 | Passive check (unhealthy node) When TCP type is checked, determine the number of times that the node is not healthy. | -| upstream.checks.passive.unhealthy.timeouts | Passive check (unhealthy node) | integer | `1` to `254` | 7 | Passive checks (unhealthy node) determine the number of timeouts for unhealthy nodes. | -| upstream.checks.passive.unhealthy.http_failures | Passive check (unhealthy node) | integer | `1` to `254` | 5 | Passive check (unhealthy node) The number of times that the node is not healthy during HTTP or HTTPS type checking. | +| upstream.checks.passive.unhealthy.tcp_failures | Passive check (unhealthy node) | integer | `0` to `254` | 2 | Passive check (unhealthy node) When TCP type is checked, determine the number of times that the node is not healthy. | +| upstream.checks.passive.unhealthy.timeouts | Passive check (unhealthy node) | integer | `0` to `254` | 7 | Passive checks (unhealthy node) determine the number of timeouts for unhealthy nodes. | +| upstream.checks.passive.unhealthy.http_failures | Passive check (unhealthy node) | integer | `0` to `254` | 5 | Passive check (unhealthy node) The number of times that the node is not healthy during HTTP or HTTPS type checking. | ### Configuration example diff --git a/docs/zh/latest/health-check.md b/docs/zh/latest/health-check.md index 4e3f9928bb4d..a6d2ecd4e4e1 100644 --- a/docs/zh/latest/health-check.md +++ b/docs/zh/latest/health-check.md @@ -53,11 +53,11 @@ Apache APISIX 的健康检查使用 [lua-resty-healthcheck](https://github.com/K | upstream.checks.active.unhealthy.tcp_failures | 主动检查(非健康节点) | integer | `1` 至 `254` | 2 | 主动检查(非健康节点)TCP 类型检查时,确定节点非健康的次数。 | | upstream.checks.active.unhealthy.timeouts | 主动检查(非健康节点) | integer | `1` 至 `254` | 3 | 主动检查(非健康节点)确定节点非健康的超时次数。 | | upstream.checks.passive.healthy.http_statuses | 被动检查(健康节点) | array | `200` 至 `599` | [200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308] | 被动检查(健康节点) HTTP 或 HTTPS 类型检查时,健康节点的HTTP状态码。 | -| upstream.checks.passive.healthy.successes | 被动检查(健康节点) | integer | `1` 至 `254` | 5 | 被动检查(健康节点)确定节点健康的次数。 | +| upstream.checks.passive.healthy.successes | 被动检查(健康节点) | integer | `0` 至 `254` | 5 | 被动检查(健康节点)确定节点健康的次数。 | | upstream.checks.passive.unhealthy.http_statuses | 被动检查(非健康节点) | array | `200` 至 `599` | [429, 500, 503] | 被动检查(非健康节点) HTTP 或 HTTPS 类型检查时,非健康节点的HTTP状态码。 | -| upstream.checks.passive.unhealthy.tcp_failures | 被动检查(非健康节点) | integer | `1` 至 `254` | 2 | 被动检查(非健康节点)TCP 类型检查时,确定节点非健康的次数。 | -| upstream.checks.passive.unhealthy.timeouts | 被动检查(非健康节点) | integer | `1` 至 `254` | 7 | 被动检查(非健康节点)确定节点非健康的超时次数。 | -| upstream.checks.passive.unhealthy.http_failures | 被动检查(非健康节点) | integer | `1` 至 `254` | 5 | 被动检查(非健康节点)HTTP 或 HTTPS 类型检查时,确定节点非健康的次数。 | +| upstream.checks.passive.unhealthy.tcp_failures | 被动检查(非健康节点) | integer | `0` 至 `254` | 2 | 被动检查(非健康节点)TCP 类型检查时,确定节点非健康的次数。 | +| upstream.checks.passive.unhealthy.timeouts | 被动检查(非健康节点) | integer | `0` 至 `254` | 7 | 被动检查(非健康节点)确定节点非健康的超时次数。 | +| upstream.checks.passive.unhealthy.http_failures | 被动检查(非健康节点) | integer | `0` 至 `254` | 5 | 被动检查(非健康节点)HTTP 或 HTTPS 类型检查时,确定节点非健康的次数。 | ### 配置示例: diff --git a/t/admin/health-check.t b/t/admin/health-check.t index 22d0e5e75979..f4246f371b52 100644 --- a/t/admin/health-check.t +++ b/t/admin/health-check.t @@ -52,6 +52,15 @@ add_block_preprocessor(sub { _EOC_ $block->set_value("init_by_lua_block", $init_by_lua_block); + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + }); run_tests; @@ -90,12 +99,8 @@ __DATA__ ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -137,12 +142,8 @@ passed ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -166,13 +167,9 @@ passed ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"active\" validation failed: property \"healthy\" validation failed: property \"successes\" validation failed: expected 255 to be smaller than 254"} ---- no_error_log -[error] @@ -196,13 +193,9 @@ GET /t ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"active\" validation failed: property \"healthy\" validation failed: property \"successes\" validation failed: expected 0 to be greater than 1"} ---- no_error_log -[error] @@ -226,13 +219,9 @@ GET /t ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"passive\" validation failed: property \"unhealthy\" validation failed: property \"http_statuses\" validation failed: failed to validate item 2: expected 600 to be smaller than 599"} ---- no_error_log -[error] @@ -254,13 +243,9 @@ GET /t ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"active\" validation failed: property \"type\" validation failed: matches none of the enum values"} ---- no_error_log -[error] @@ -284,13 +269,9 @@ GET /t ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"active\" validation failed: property \"healthy\" validation failed: property \"http_statuses\" validation failed: expected unique items but items 1 and 2 are equal"} ---- no_error_log -[error] @@ -314,13 +295,9 @@ GET /t ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"active\" validation failed: property \"unhealthy\" validation failed: property \"http_failures\" validation failed: wrong type: expected integer, got number"} ---- no_error_log -[error] @@ -353,12 +330,8 @@ GET /t ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -391,12 +364,8 @@ passed ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -429,13 +398,9 @@ passed ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: property \"active\" validation failed: property \"req_headers\" validation failed: failed to validate item 2: wrong type: expected string, got number"} ---- no_error_log -[error] @@ -469,17 +434,64 @@ GET /t ngx.print(body) } } ---- request -GET /t --- error_code: 400 --- response_body {"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"checks\" validation failed: object matches none of the requireds: [\"active\"] or [\"active\",\"passive\"]"} ---- no_error_log -[error] -=== TEST 13: number type timeout +=== TEST 13: only active +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + req_data.upstream.checks = json.decode([[{ + "active": { + "http_path": "/status", + "host": "foo.com", + "healthy": { + "interval": 2, + "successes": 1 + }, + "unhealthy": { + "interval": 1, + "http_failures": 2 + } + } + }]]) + exp_data.node.value.upstream.checks.active = req_data.upstream.checks.active + exp_data.node.value.upstream.checks.passive = { + type = "http", + healthy = { + http_statuses = { 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, + 300, 301, 302, 303, 304, 305, 306, 307, 308 }, + successes = 0, + }, + unhealthy = { + http_statuses = { 429, 500, 503 }, + tcp_failures = 0, + timeouts = 0, + http_failures = 0, + } + } + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + req_data, + exp_data + ) + + ngx.status = code + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 14: number type timeout --- config location /t { content_by_lua_block { @@ -512,9 +524,5 @@ GET /t ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] From f0643b1b806aff87175a510716bece5f02088f21 Mon Sep 17 00:00:00 2001 From: besich Date: Wed, 24 Nov 2021 15:56:05 +0800 Subject: [PATCH 123/260] docs: update the docs/zh/latest/README.md (#5583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 琚致远 Co-authored-by: liangfei Co-authored-by: 罗泽轩 --- docs/zh/latest/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index 9164d0f4d576..f58a79e05efb 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -144,6 +144,16 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - 自定义负载均衡算法:可以在 `balancer` 阶段使用自定义负载均衡算法。 - 自定义路由: 支持用户自己实现路由算法。 +- **多语言支持** + - Apache APISIX 是一个通过 `WASM` 和 `RPC` 支持不同语言来进行插件开发的网关. + ![Multi Language Support into Apache APISIX](../../../docs/assets/images/apisix-multi-lang-support.png) + - WASM 或 WebAssembly 是比较现代的开发方式。 APISIX 能加载运行使用[Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks)编译的 WASM 字节码。开发者仅需要使用该 SDK 编写代码,然后编译成 WASM 字节码,即可运行在 APISIX 中的 WASM 虚拟机中。 + - RPC 是一种比较传统的开发方式。开发者可以使用他们需要的语言来进行 RPC 服务的开发,该 RPC 通过本地通讯来跟 APISIX 进行数据交换。到目前为止,APISIX 已支持[Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) and Node.js. + +- **Serverless** + - [Lua functions](plugins/serverless.md): 能在 APISIX 每个阶段调用 lua 函数. + - [Azure functions](docs/en/latest/plugins/azure-functions.md): 能无缝整合进 Azure Serverless Function 中。作为动态上游,能将特定的 URI 请求全部代理到微软 Azure 云中。 + ## 立刻开始 1. 安装 @@ -221,7 +231,9 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 ### 贡献者变化 -![contributor-over-time](../../assets/images/contributor-over-time.png) +> [访问此处](https://www.apiseven.com/contributor-graph) 使用贡献者数据服务。 + +[![贡献者变化](https://contributor-graph-api.apiseven.com/contributors-svg?repo=apache/apisix)](https://www.apiseven.com/en/contributor-graph?repo=apache/apisix) ## 视频和文章 From 4b94b84f92d1cb6cb58e52075637a42ed76457d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Wed, 24 Nov 2021 19:23:52 +0800 Subject: [PATCH 124/260] docs: add google cloud logging document to README (#5598) --- README.md | 2 +- docs/zh/latest/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e68ea89becd..d14aae9cc992 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - High performance: The single-core QPS reaches 18k with an average delay of fewer than 0.2 milliseconds. - [Fault Injection](docs/en/latest/plugins/fault-injection.md) - [REST Admin API](docs/en/latest/admin-api.md): Using the REST Admin API to control Apache APISIX, which only allows 127.0.0.1 access by default, you can modify the `allow_admin` field in `conf/config.yaml` to specify a list of IPs that are allowed to call the Admin API. Also, note that the Admin API uses key auth to verify the identity of the caller. **The `admin_key` field in `conf/config.yaml` needs to be modified before deployment to ensure security**. - - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md)) + - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md)) - [Datadog](docs/en/latest/plugins/datadog.md): push custom metrics to the DogStatsD server, comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server. - [Helm charts](https://github.com/apache/apisix-helm-chart) diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index f58a79e05efb..7f8a1e24d9aa 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -135,7 +135,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - 高性能:在单核上 QPS 可以达到 18k,同时延迟只有 0.2 毫秒。 - [故障注入](plugins/fault-injection.md) - [REST Admin API](admin-api.md): 使用 REST Admin API 来控制 Apache APISIX,默认只允许 127.0.0.1 访问,你可以修改 `conf/config.yaml` 中的 `allow_admin` 字段,指定允许调用 Admin API 的 IP 列表。同时需要注意的是,Admin API 使用 key auth 来校验调用者身份,**在部署前需要修改 `conf/config.yaml` 中的 `admin_key` 字段,来保证安全。** - - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md)) + - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md), [Google Cloud Logging](plugins/google-cloud-logging.md)) - [Helm charts](https://github.com/apache/apisix-helm-chart) - **高度可扩展** From 12b3fac3cef09347aeae1c41baaa7095c8c8ab5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Thu, 25 Nov 2021 10:23:54 +0800 Subject: [PATCH 125/260] docs: update admin ip example node ip (#5603) --- docs/en/latest/admin-api.md | 70 ++++++++++++++++++------------------- docs/zh/latest/admin-api.md | 70 ++++++++++++++++++------------------- 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index b5f4cacf1308..20c09334636d 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -142,7 +142,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -158,7 +158,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/2?ttl=60 -H 'X-API-KEY: edd1c9f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -173,7 +173,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H'X-API-KEY: edd1c9f034335f1 { "upstream": { "nodes": { - "39.97.63.216:80": 1 + "127.0.0.1:1981": 1 } } }' @@ -182,8 +182,8 @@ HTTP/1.1 200 OK After successful execution, upstream nodes will be updated to: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 1 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 1 } @@ -192,7 +192,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H'X-API-KEY: edd1c9f034335f1 { "upstream": { "nodes": { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } } }' @@ -201,8 +201,8 @@ HTTP/1.1 200 OK After successful execution, upstream nodes will be updated to: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 10 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 10 } @@ -211,7 +211,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H'X-API-KEY: edd1c9f034335f1 { "upstream": { "nodes": { - "39.97.63.215:80": null + "127.0.0.1:1980": null } } }' @@ -220,7 +220,7 @@ HTTP/1.1 200 OK After successful execution, upstream nodes will be updated to: { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } @@ -238,14 +238,14 @@ After successful execution, methods will not retain the original data, and the e # Replace upstream nodes of the Route -- sub path $ curl http://127.0.0.1:9080/apisix/admin/routes/1/upstream/nodes -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 }' HTTP/1.1 200 OK ... After successful execution, nodes will not retain the original data, and the entire update is: { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 } @@ -359,7 +359,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H 'X-API-KEY: edd1c9f03 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -373,7 +373,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H'X-API-KEY: edd1c9f0343 { "upstream": { "nodes": { - "39.97.63.216:80": 1 + "127.0.0.1:1981": 1 } } }' @@ -382,8 +382,8 @@ HTTP/1.1 200 OK After successful execution, upstream nodes will be updated to: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 1 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 1 } @@ -392,7 +392,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H'X-API-KEY: edd1c9f0343 { "upstream": { "nodes": { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } } }' @@ -401,8 +401,8 @@ HTTP/1.1 200 OK After successful execution, upstream nodes will be updated to: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 10 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 10 } @@ -411,7 +411,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H'X-API-KEY: edd1c9f0343 { "upstream": { "nodes": { - "39.97.63.215:80": null + "127.0.0.1:1980": null } } }' @@ -420,21 +420,21 @@ HTTP/1.1 200 OK After successful execution, upstream nodes will be updated to: { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } # Replace upstream nodes of the Service $ curl http://127.0.0.1:9080/apisix/admin/services/201/upstream/nodes -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 }' HTTP/1.1 200 OK ... After successful execution, upstream nodes will not retain the original data, and the entire update is: { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 } ``` @@ -622,7 +622,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H 'X-API-KEY: edd1c9f0 { "type":"roundrobin", "nodes":{ - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } }' HTTP/1.1 201 Created @@ -633,7 +633,7 @@ HTTP/1.1 201 Created $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { "nodes": { - "39.97.63.216:80": 1 + "127.0.0.1:1981": 1 } }' HTTP/1.1 200 OK @@ -641,8 +641,8 @@ HTTP/1.1 200 OK After successful execution, nodes will be updated to: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 1 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 1 } @@ -650,7 +650,7 @@ After successful execution, nodes will be updated to: $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { "nodes": { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } }' HTTP/1.1 200 OK @@ -658,8 +658,8 @@ HTTP/1.1 200 OK After successful execution, nodes will be updated to: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 10 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 10 } @@ -667,7 +667,7 @@ After successful execution, nodes will be updated to: $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { "nodes": { - "39.97.63.215:80": null + "127.0.0.1:1980": null } }' HTTP/1.1 200 OK @@ -675,21 +675,21 @@ HTTP/1.1 200 OK After successful execution, nodes will be updated to: { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } # Replace the nodes of the Upstream $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100/nodes -H'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 }' HTTP/1.1 200 OK ... After the execution is successful, nodes will not retain the original data, and the entire update is: { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 } ``` @@ -750,7 +750,7 @@ For example: "type": "roundrobin", "nodes": [ {"host": "127.0.0.1", "port": 1980, "weight": 2000}, - {"host": "127.0.0.2", "port": 1980, "weight": 1, "priority": -1} + {"host": "127.0.0.1", "port": 1981, "weight": 1, "priority": -1} ], "checks": { "active": { diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index f036fd26239f..fa7f0d65fecc 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -143,7 +143,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -159,7 +159,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/2?ttl=60 -H 'X-API-KEY: edd1c9f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -174,7 +174,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f { "upstream": { "nodes": { - "39.97.63.216:80": 1 + "127.0.0.1:1981": 1 } } }' @@ -183,8 +183,8 @@ HTTP/1.1 200 OK 执行成功后,upstream nodes 将更新为: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 1 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 1 } @@ -193,7 +193,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f { "upstream": { "nodes": { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } } }' @@ -202,8 +202,8 @@ HTTP/1.1 200 OK 执行成功后,upstream nodes 将更新为: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 10 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 10 } @@ -212,7 +212,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f { "upstream": { "nodes": { - "39.97.63.215:80": null + "127.0.0.1:1980": null } } }' @@ -221,7 +221,7 @@ HTTP/1.1 200 OK 执行成功后,upstream nodes 将更新为: { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } @@ -239,14 +239,14 @@ HTTP/1.1 200 OK # 替换路由的 upstream nodes -- sub path $ curl http://127.0.0.1:9080/apisix/admin/routes/1/upstream/nodes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 }' HTTP/1.1 200 OK ... 执行成功后,nodes 将不保留原来的数据,整个更新为: { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 } @@ -362,7 +362,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H 'X-API-KEY: edd1c9f03 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -378,7 +378,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H 'X-API-KEY: edd1c9f034 { "upstream": { "nodes": { - "39.97.63.216:80": 1 + "127.0.0.1:1981": 1 } } }' @@ -387,8 +387,8 @@ HTTP/1.1 200 OK 执行成功后,upstream nodes 将更新为: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 1 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 1 } @@ -397,7 +397,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H 'X-API-KEY: edd1c9f034 { "upstream": { "nodes": { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } } }' @@ -406,8 +406,8 @@ HTTP/1.1 200 OK 执行成功后,upstream nodes 将更新为: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 10 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 10 } @@ -416,7 +416,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/201 -H 'X-API-KEY: edd1c9f034 { "upstream": { "nodes": { - "39.97.63.215:80": null + "127.0.0.1:1980": null } } }' @@ -425,21 +425,21 @@ HTTP/1.1 200 OK 执行成功后,upstream nodes 将更新为: { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } # 替换 Service 的 upstream nodes $ curl http://127.0.0.1:9080/apisix/admin/services/201/upstream/nodes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 }' HTTP/1.1 200 OK ... 执行成功后,upstream nodes 将不保留原来的数据,整个更新为: { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 } ``` @@ -627,7 +627,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H 'X-API-KEY: edd1c9f0 { "type":"roundrobin", "nodes":{ - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } }' HTTP/1.1 201 Created @@ -638,7 +638,7 @@ HTTP/1.1 201 Created $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { "nodes": { - "39.97.63.216:80": 1 + "127.0.0.1:1981": 1 } }' HTTP/1.1 200 OK @@ -646,8 +646,8 @@ HTTP/1.1 200 OK 执行成功后,nodes 将更新为: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 1 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 1 } @@ -655,7 +655,7 @@ HTTP/1.1 200 OK $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { "nodes": { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } }' HTTP/1.1 200 OK @@ -663,8 +663,8 @@ HTTP/1.1 200 OK 执行成功后,nodes 将更新为: { - "39.97.63.215:80": 1, - "39.97.63.216:80": 10 + "127.0.0.1:1980": 1, + "127.0.0.1:1981": 10 } @@ -672,7 +672,7 @@ HTTP/1.1 200 OK $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { "nodes": { - "39.97.63.215:80": null + "127.0.0.1:1980": null } }' HTTP/1.1 200 OK @@ -680,21 +680,21 @@ HTTP/1.1 200 OK 执行成功后,nodes 将更新为: { - "39.97.63.216:80": 10 + "127.0.0.1:1981": 10 } # 替换 Upstream 的 nodes $ curl http://127.0.0.1:9080/apisix/admin/upstreams/100/nodes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PATCH -i -d ' { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 }' HTTP/1.1 200 OK ... 执行成功后,nodes 将不保留原来的数据,整个更新为: { - "39.97.63.200:80": 1 + "127.0.0.1:1982": 1 } ``` @@ -753,7 +753,7 @@ $ curl http://127.0.0.1:9080/get "type": "roundrobin", "nodes": [ {"host": "127.0.0.1", "port": 1980, "weight": 2000}, - {"host": "127.0.0.2", "port": 1980, "weight": 1, "priority": -1} + {"host": "127.0.0.1", "port": 1981, "weight": 1, "priority": -1} ], "checks": { "active": { From 783b387f8167a52d5df8fa02c46d72fcab9b506c Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Thu, 25 Nov 2021 15:20:43 +0800 Subject: [PATCH 126/260] ci(bug): Improve DNS settings (#5601) CentOS docker: use `--dns` parameter Ubuntu: use `netplan` --- .github/workflows/centos7-ci.yml | 2 +- utils/set-dns.sh | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 5f9f6ec742b1..db5ad0b1e041 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -57,7 +57,7 @@ jobs: - name: Run centos7 docker and mapping apisix into container run: | - docker run -itd -v /home/runner/work/apisix/apisix:/apisix --name centos7Instance --net="host" docker.io/centos:7 /bin/bash + docker run -itd -v /home/runner/work/apisix/apisix:/apisix --name centos7Instance --net="host" --dns 8.8.8.8 --dns-search apache.org docker.io/centos:7 /bin/bash # docker exec centos7Instance bash -c "cp -r /tmp/apisix ./" - name: Run other docker containers for test diff --git a/utils/set-dns.sh b/utils/set-dns.sh index 53970a7a285c..2c7689dfc37d 100755 --- a/utils/set-dns.sh +++ b/utils/set-dns.sh @@ -25,8 +25,24 @@ echo "127.0.0.1 test.com" | sudo tee -a /etc/hosts echo "127.0.0.1 admin.apisix.dev" | sudo tee -a /etc/hosts cat /etc/hosts # check GitHub Action's configuration -echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf -echo "search apache.org" | sudo tee -a /etc/resolv.conf +# override DNS configures +if [ -f "/etc/netplan/50-cloud-init.yaml" ]; then + sudo pip3 install yq + + tmp=$(mktemp) + yq -y '.network.ethernets.eth0."dhcp4-overrides"."use-dns"=false' /etc/netplan/50-cloud-init.yaml | \ + yq -y '.network.ethernets.eth0."dhcp4-overrides"."use-domains"=false' | \ + yq -y '.network.ethernets.eth0.nameservers.addresses[0]="8.8.8.8"' | \ + yq -y '.network.ethernets.eth0.nameservers.search[0]="apache.org"' > $tmp + mv $tmp /etc/netplan/50-cloud-init.yaml + cat /etc/netplan/50-cloud-init.yaml + sudo netplan apply + sleep 3 + + sudo mv /etc/resolv.conf /etc/resolv.conf.bak + sudo ln -s /run/systemd/resolve/resolv.conf /etc/ +fi +cat /etc/resolv.conf mkdir -p build-cache From 4f02605f3f0230847bbf96f3699e764371ba213f Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Thu, 25 Nov 2021 16:53:28 +0800 Subject: [PATCH 127/260] feat: Apache OpenWhisk plugin (#5518) --- apisix/core/request.lua | 3 + apisix/plugins/openwhisk.lua | 114 +++++++++++++ ci/linux-ci-init-service.sh | 6 + conf/config-default.yaml | 1 + docs/en/latest/config.json | 3 +- docs/en/latest/plugins/openwhisk.md | 98 +++++++++++ t/admin/plugins.t | 2 +- t/core/request.t | 27 +++ t/plugin/openwhisk.t | 245 ++++++++++++++++++++++++++++ 9 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 apisix/plugins/openwhisk.lua create mode 100644 docs/en/latest/plugins/openwhisk.md create mode 100644 t/plugin/openwhisk.t diff --git a/apisix/core/request.lua b/apisix/core/request.lua index 18d997845b7a..95d84b95b4f6 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -273,4 +273,7 @@ function _M.get_http_version() return ngx.req.http_version() end + +_M.get_method = ngx.req.get_method + return _M diff --git a/apisix/plugins/openwhisk.lua b/apisix/plugins/openwhisk.lua new file mode 100644 index 000000000000..a8fe8c2f74a2 --- /dev/null +++ b/apisix/plugins/openwhisk.lua @@ -0,0 +1,114 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +local core = require("apisix.core") +local http = require("resty.http") +local ngx_encode_base64 = ngx.encode_base64 +local tostring = tostring + +local schema = { + type = "object", + properties = { + api_host = {type = "string"}, + ssl_verify = { + type = "boolean", + default = true, + }, + service_token = {type = "string"}, + namespace = {type = "string", maxLength = 256}, + action = {type = "string", maxLength = 256}, + result = { + type = "boolean", + default = true, + }, + timeout = { + type = "integer", + minimum = 1, + maximum = 60000, + default = 3000, + description = "timeout in milliseconds", + }, + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 5} + }, + required = {"api_host", "service_token", "namespace", "action"} +} + + +local _M = { + version = 0.1, + priority = -1901, + name = "openwhisk", + schema = schema, +} + + +function _M.check_schema(conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + return true +end + + +function _M.access(conf, ctx) + local params = { + method = "POST", + body = core.request.get_body(), + query = { + blocking = "true", + result = tostring(conf.result), + timeout = conf.timeout + }, + headers = { + ["Authorization"] = "Basic " .. ngx_encode_base64(conf.service_token), + ["Content-Type"] = "application/json", + }, + keepalive = conf.keepalive, + ssl_verify = conf.ssl_verify + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + end + + -- OpenWhisk action endpoint + local endpoint = conf.api_host .. "/api/v1/namespaces/" .. conf.namespace .. + "/actions/" .. conf.action + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local res, err = httpc:request_uri(endpoint, params) + + if not res or err then + core.log.error("failed to process openwhisk action, err: ", err) + return 503 + end + + -- setting response headers + core.response.set_header(res.headers) + + return res.status, res.body +end + + +return _M diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 0d15d06042e0..83144b7b3b30 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -19,3 +19,9 @@ docker exec -i apache-apisix_kafka-server1_1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 1 --topic test2 docker exec -i apache-apisix_kafka-server1_1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server1:2181 --replication-factor 1 --partitions 3 --topic test3 docker exec -i apache-apisix_kafka-server2_1 /opt/bitnami/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper-server2:2181 --replication-factor 1 --partitions 1 --topic test4 + +# prepare openwhisk env +docker pull openwhisk/action-nodejs-v14:nightly +docker run --rm -d --name openwhisk -p 3233:3233 -p 3232:3232 -v /var/run/docker.sock:/var/run/docker.sock openwhisk/standalone:nightly +docker exec -i openwhisk waitready +docker exec -i openwhisk bash -c "wsk action update test <(echo 'function main(args){return {\"hello\":args.name || \"test\"}}') --kind nodejs:14" diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 4e385ed3e48d..dc42e04bea5c 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -356,6 +356,7 @@ plugins: # plugin list (sorted by priority) - example-plugin # priority: 0 #- skywalking # priority: -1100 - azure-functions # priority: -1900 + - openwhisk # priority: -1901 - serverless-post-function # priority: -2000 - ext-plugin-post-req # priority: -3000 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 09513d276d09..68631c161b88 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -129,7 +129,8 @@ "label": "Serverless", "items": [ "plugins/serverless", - "plugins/azure-functions" + "plugins/azure-functions", + "plugins/openwhisk" ] }, { diff --git a/docs/en/latest/plugins/openwhisk.md b/docs/en/latest/plugins/openwhisk.md new file mode 100644 index 000000000000..5bc65595f318 --- /dev/null +++ b/docs/en/latest/plugins/openwhisk.md @@ -0,0 +1,98 @@ +--- +title: openwhisk +--- + + + +## Summary + +- [**Description**](#description) +- [**Attributes**](#attributes) +- [**Example**](#example) + +## Description + +The `openwhisk` plugin is used to support integration with the [Apache OpenWhisk](https://openwhisk.apache.org) serverless platform and can be set up on a route in place of Upstream, which will take over the request and send it to the OpenWhisk API endpoint. + +Users can call the OpenWhisk action via APISIX, pass the request parameters via JSON and get the response content. + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| -- | -- | -- | -- | -- | -- | +| api_host | string | required |   |   | OpenWhisk API host (eg. https://localhost:3233) | +| ssl_verify | boolean | optional | true | | Whether to verify the certificate | +| service_token | string | required |   |   | OpenWhisk ServiceToken (The format is `xxx:xxx`,Passed through Basic Auth when calling the API) | +| namespace | string | required |   |   | OpenWhisk Namespace (eg. guest) | +| action | string | required |   |   | OpenWhisk Action (eg. hello) | +| result | boolean | optional | true |   | Whether to get Action metadata (default to execute function and get response; false to get Action metadata but not execute Action, including runtime, function body, restrictions, etc.) | +| timeout | integer | optional | 60000ms | [1, 60000]ms | OpenWhisk Action and HTTP call timeout. | +| keepalive | boolean | optional | true |   | HTTP keepalive | +| keepalive_timeout | integer | optional | 60000ms | [1000,...] | keepalive idle timeout | +| keepalive_pool | integer | optional | 5 | [1,...] | Connection pool limit | + +:::note + +- The `timeout` property controls both the time taken by the OpenWhisk Action to execute and the timeout of the HTTP client in APISIX. OpenWhisk Action calls may consume time on pulling the runtime image and starting the container, so if you set the value too small, you may cause a large number of requests to fail. OpenWhisk supports timeouts ranging from 1ms to 60000ms, and we recommended to set at least 1000ms or more. + +::: + +## Example + +First, you need to run the OpenWhisk environment. Here is an example of using OpenWhisk standalone mode. + +```shell +docker run --rm -d \ + -h openwhisk --name openwhisk \ + -p 3233:3233 -p 3232:3232 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + openwhisk/standalone:nightly +docker exec openwhisk waitready +``` + +Then, you need to create an Action for testing. + +```shell +wsk property set --apihost "http://localhost:3233" --auth "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP" +wsk action update test <(echo 'function main(){return {"ready":true}}') --kind nodejs:14 +``` + +Here is an example of creating a Route and enabling this plugin + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/hello", + "plugins": { + "openwhisk": { + "api_host": "http://localhost:3233", + "service_token": "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP", + "namespace": "guest", + "action": "test" + } + } +}' +``` + +Finally, you can send a request to this route and you will get the following response. And you can disable it by removing the openwhsik plugin from the route. + +```json +{"ready": true} +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 2c62bcafc9d7..f20b8b98ef93 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -40,7 +40,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","google-cloud-logging","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","azure-functions","serverless-post-function","ext-plugin-post-req"\]/ +qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","google-cloud-logging","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","azure-functions","openwhisk","serverless-post-function","ext-plugin-post-req"\]/ --- no_error_log [error] diff --git a/t/core/request.t b/t/core/request.t index 06256dcc9a4b..feb7ac5afde9 100644 --- a/t/core/request.t +++ b/t/core/request.t @@ -437,3 +437,30 @@ c=z_z&v=x%20x nil --- error_log the post form is too large: request body in temp file not supported + + + +=== TEST 13: get_method +--- config + location = /hello { + content_by_lua_block { + local core = require("apisix.core") + local ngx_ctx = ngx.ctx + local api_ctx = ngx_ctx.api_ctx + if api_ctx == nil then + api_ctx = core.tablepool.fetch("api_ctx", 0, 32) + ngx_ctx.api_ctx = api_ctx + end + + core.ctx.set_vars_meta(api_ctx) + + local method = core.request.get_method(ngx.ctx.api_ctx) + ngx.say(method) + } + } +--- request +POST /hello +--- response_body +POST +--- no_error_log +[error] diff --git a/t/plugin/openwhisk.t b/t/plugin/openwhisk.t new file mode 100644 index 000000000000..e20fe5a5547e --- /dev/null +++ b/t/plugin/openwhisk.t @@ -0,0 +1,245 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: sanity check with minimal valid configuration. +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openwhisk") + local ok, err = plugin.check_schema({api_host = "http://127.0.0.1:3233", service_token = "test:test", namespace = "test", action = "test"}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 2: missing `api_host` +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openwhisk") + local ok, err = plugin.check_schema({service_token = "test:test", namespace = "test", action = "test"}) + if not ok then + ngx.say(err) + end + } + } +--- response_body +property "api_host" is required + + + +=== TEST 3: wrong type for `api_host` +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.openwhisk") + local ok, err = plugin.check_schema({api_host = 3233, service_token = "test:test", namespace = "test", action = "test"}) + if not ok then + ngx.say(err) + end + } + } +--- response_body +property "api_host" validation failed: wrong type: expected string, got number + + + +=== TEST 4: setup route with plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "openwhisk": { + "api_host": "http://127.0.0.1:3233", + "service_token": "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP", + "namespace": "guest", + "action": "test" + } + }, + "upstream": { + "nodes": {}, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: hit route (with GET request) +--- request +GET /hello +--- response_body chomp +{"hello":"test"} + + + +=== TEST 6: hit route (with POST method and non-json format request body) +--- request +POST /hello +test=test +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- error_code: 400 +--- response_body_like eval +qr/"error":"The request content was malformed/ + + + +=== TEST 7: hit route (with POST and correct request body) +--- request +POST /hello +{"name": "world"} +--- more_headers +Content-Type: application/json +--- response_body chomp +{"hello":"world"} + + + +=== TEST 8: reset route to non-existent action +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "openwhisk": { + "api_host": "http://127.0.0.1:3233", + "service_token": "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP", + "namespace": "guest", + "action": "non-existent" + } + }, + "upstream": { + "nodes": {}, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: hit route (with non-existent action) +--- request +POST /hello +{"name": "world"} +--- more_headers +Content-Type: application/json +--- error_code: 404 +--- response_body_like eval +qr/"error":"The requested resource does not exist."/ + + + +=== TEST 10: reset route to wrong api_host +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "openwhisk": { + "api_host": "http://127.0.0.0:3233", + "service_token": "23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP", + "namespace": "guest", + "action": "non-existent" + } + }, + "upstream": { + "nodes": {}, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 11: hit route (with wrong api_host) +--- request +POST /hello +{"name": "world"} +--- more_headers +Content-Type: application/json +--- error_code: 503 +--- error_log +failed to process openwhisk action, err: From c5bde40feb0fd20674af3ced58fcabbcb35f701c Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Fri, 26 Nov 2021 11:09:23 +0800 Subject: [PATCH 128/260] docs: add OpenWhisk plugin to README (#5611) --- README.md | 1 + docs/zh/latest/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index d14aae9cc992..abce5e2a5733 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - **Serverless** - [Lua functions](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX. - [Azure functions](docs/en/latest/plugins/azure-functions.md): seamless integration with Azure Serverless Function as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud. + - [Apache OpenWhisk](docs/en/latest/plugins/openwhisk.md): seamless integration with Apache OpenWhisk as a dynamic upstream to proxy all requests for a particular URI to your own OpenWhisk cluster. ## Get Started diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index 7f8a1e24d9aa..c8a3a654cafa 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -153,6 +153,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - **Serverless** - [Lua functions](plugins/serverless.md): 能在 APISIX 每个阶段调用 lua 函数. - [Azure functions](docs/en/latest/plugins/azure-functions.md): 能无缝整合进 Azure Serverless Function 中。作为动态上游,能将特定的 URI 请求全部代理到微软 Azure 云中。 + - [Apache OpenWhisk](docs/en/latest/plugins/openwhisk.md): 与Apache OpenWhisk集成。作为动态上游,能将特定的 URI 请求代理到你自己的 OpenWhisk 集群。 ## 立刻开始 From 54f15156a00c78ef9d95f6b67bcc09721af16f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 26 Nov 2021 11:09:39 +0800 Subject: [PATCH 129/260] docs: don't use real ip in the example (#5606) --- docs/en/latest/architecture-design/plugin-config.md | 6 +++--- docs/en/latest/architecture-design/route.md | 6 +++--- docs/en/latest/architecture-design/service.md | 2 +- docs/en/latest/architecture-design/upstream.md | 4 ++-- docs/en/latest/control-api.md | 8 ++++---- docs/en/latest/plugin-develop.md | 2 +- docs/en/latest/plugins/client-control.md | 4 ++-- docs/en/latest/plugins/ext-plugin-pre-req.md | 4 ++-- docs/en/latest/plugins/hmac-auth.md | 4 ++-- docs/en/latest/plugins/ip-restriction.md | 2 +- docs/en/latest/plugins/jwt-auth.md | 4 ++-- docs/en/latest/plugins/key-auth.md | 4 ++-- docs/en/latest/plugins/limit-conn.md | 6 +++--- docs/en/latest/plugins/limit-count.md | 6 +++--- docs/en/latest/plugins/limit-req.md | 2 +- docs/en/latest/plugins/referer-restriction.md | 2 +- docs/en/latest/plugins/serverless.md | 4 ++-- docs/en/latest/plugins/ua-restriction.md | 2 +- docs/en/latest/plugins/zipkin.md | 4 ++-- docs/en/latest/router-radixtree.md | 6 +++--- docs/zh/latest/architecture-design/plugin-config.md | 6 +++--- docs/zh/latest/architecture-design/route.md | 6 +++--- docs/zh/latest/architecture-design/service.md | 2 +- docs/zh/latest/architecture-design/upstream.md | 4 ++-- docs/zh/latest/plugin-develop.md | 2 +- docs/zh/latest/plugins/client-control.md | 4 ++-- docs/zh/latest/plugins/ext-plugin-pre-req.md | 4 ++-- docs/zh/latest/plugins/hmac-auth.md | 4 ++-- docs/zh/latest/plugins/ip-restriction.md | 2 +- docs/zh/latest/plugins/jwt-auth.md | 4 ++-- docs/zh/latest/plugins/key-auth.md | 4 ++-- docs/zh/latest/plugins/limit-conn.md | 6 +++--- docs/zh/latest/plugins/limit-count.md | 8 ++++---- docs/zh/latest/plugins/limit-req.md | 6 +++--- docs/zh/latest/plugins/referer-restriction.md | 2 +- docs/zh/latest/plugins/serverless.md | 4 ++-- docs/zh/latest/plugins/ua-restriction.md | 2 +- docs/zh/latest/plugins/zipkin.md | 4 ++-- docs/zh/latest/router-radixtree.md | 6 +++--- 39 files changed, 81 insertions(+), 81 deletions(-) diff --git a/docs/en/latest/architecture-design/plugin-config.md b/docs/en/latest/architecture-design/plugin-config.md index c3d7be27b927..84de44879c44 100644 --- a/docs/en/latest/architecture-design/plugin-config.md +++ b/docs/en/latest/architecture-design/plugin-config.md @@ -48,7 +48,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -89,7 +89,7 @@ to "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } "plugins": { @@ -116,7 +116,7 @@ is equal to "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } "plugins": { diff --git a/docs/en/latest/architecture-design/route.md b/docs/en/latest/architecture-design/route.md index e6804424b854..57a2449ccd3d 100644 --- a/docs/en/latest/architecture-design/route.md +++ b/docs/en/latest/architecture-design/route.md @@ -33,7 +33,7 @@ We configure all the parameters directly in the Route, it's easy to set up, and The shortcomings mentioned above are independently abstracted in APISIX by the two concepts [Service](service.md) and [Upstream](upstream.md). -The route example created below is to proxy the request with URL `/index.html` to the Upstream service with the address `39.97.63.215:80`: +The route example created below is to proxy the request with URL `/index.html` to the Upstream service with the address `127.0.0.1:1980`: ```shell $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' @@ -42,7 +42,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -54,7 +54,7 @@ Transfer-Encoding: chunked Connection: keep-alive Server: APISIX web server -{"node":{"value":{"uri":"\/index.html","upstream":{"nodes":{"39.97.63.215:80":1},"type":"roundrobin"}},"createdIndex":61925,"key":"\/apisix\/routes\/1","modifiedIndex":61925},"action":"create"} +{"node":{"value":{"uri":"\/index.html","upstream":{"nodes":{"127.0.0.1:1980":1},"type":"roundrobin"}},"createdIndex":61925,"key":"\/apisix\/routes\/1","modifiedIndex":61925},"action":"create"} ``` When we receive a successful response, it indicates that the route was successfully created. diff --git a/docs/en/latest/architecture-design/service.md b/docs/en/latest/architecture-design/service.md index df680ef01598..f2238c942e6c 100644 --- a/docs/en/latest/architecture-design/service.md +++ b/docs/en/latest/architecture-design/service.md @@ -44,7 +44,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/200 -H 'X-API-KEY: edd1c9f034 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/architecture-design/upstream.md b/docs/en/latest/architecture-design/upstream.md index 384ac8718b57..e8d8e114f7f2 100644 --- a/docs/en/latest/architecture-design/upstream.md +++ b/docs/en/latest/architecture-design/upstream.md @@ -74,7 +74,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -96,7 +96,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }, "upstream": { "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } "type": "roundrobin", "retries": 2, diff --git a/docs/en/latest/control-api.md b/docs/en/latest/control-api.md index b1e6e782bc5a..86f5e72fbdfb 100644 --- a/docs/en/latest/control-api.md +++ b/docs/en/latest/control-api.md @@ -306,9 +306,9 @@ Return all services info in the format below: "type": "roundrobin", "nodes": [ { - "port": 80, + "port": 1980, "weight": 1, - "host": "39.97.63.215" + "host": "127.0.0.1" } ] }, @@ -355,9 +355,9 @@ Return specific service info with **service_id** in the format below: "type": "roundrobin", "nodes": [ { - "port": 80, + "port": 1980, "weight": 1, - "host": "39.97.63.215" + "host": "127.0.0.1" } ] }, diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md index b6c60854b3ab..c36f5bc5cccc 100644 --- a/docs/en/latest/plugin-develop.md +++ b/docs/en/latest/plugin-develop.md @@ -348,7 +348,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/client-control.md b/docs/en/latest/plugins/client-control.md index 80e5d045d326..b0f52cd0f98f 100644 --- a/docs/en/latest/plugins/client-control.md +++ b/docs/en/latest/plugins/client-control.md @@ -58,7 +58,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -95,7 +95,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/ext-plugin-pre-req.md b/docs/en/latest/plugins/ext-plugin-pre-req.md index dd5a0cde512c..abee4b47f366 100644 --- a/docs/en/latest/plugins/ext-plugin-pre-req.md +++ b/docs/en/latest/plugins/ext-plugin-pre-req.md @@ -61,7 +61,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -91,7 +91,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/hmac-auth.md b/docs/en/latest/plugins/hmac-auth.md index 609eb5854d19..dfa66df94883 100644 --- a/docs/en/latest/plugins/hmac-auth.md +++ b/docs/en/latest/plugins/hmac-auth.md @@ -95,7 +95,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -332,7 +332,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/ip-restriction.md b/docs/en/latest/plugins/ip-restriction.md index 8012faf2b4cb..4a850742b403 100644 --- a/docs/en/latest/plugins/ip-restriction.md +++ b/docs/en/latest/plugins/ip-restriction.md @@ -164,7 +164,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index bc16d4add10e..1ed30cedcc40 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -104,7 +104,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -227,7 +227,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/key-auth.md b/docs/en/latest/plugins/key-auth.md index 25fec5f1308f..66eae4fb4da6 100644 --- a/docs/en/latest/plugins/key-auth.md +++ b/docs/en/latest/plugins/key-auth.md @@ -88,7 +88,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -142,7 +142,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/limit-conn.md b/docs/en/latest/plugins/limit-conn.md index 37f15c4609c8..ba980ef590ce 100644 --- a/docs/en/latest/plugins/limit-conn.md +++ b/docs/en/latest/plugins/limit-conn.md @@ -69,7 +69,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -95,7 +95,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -140,7 +140,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index ebd379545160..5e094d540dba 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -132,7 +132,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -162,7 +162,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -235,7 +235,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/limit-req.md b/docs/en/latest/plugins/limit-req.md index f63eb93d31a5..27761113e8ed 100644 --- a/docs/en/latest/plugins/limit-req.md +++ b/docs/en/latest/plugins/limit-req.md @@ -233,7 +233,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/referer-restriction.md b/docs/en/latest/plugins/referer-restriction.md index 098fbf9ca232..ba2d1db94e96 100644 --- a/docs/en/latest/plugins/referer-restriction.md +++ b/docs/en/latest/plugins/referer-restriction.md @@ -113,7 +113,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/serverless.md b/docs/en/latest/plugins/serverless.md index 7cde76462c3d..70871ab13f7c 100644 --- a/docs/en/latest/plugins/serverless.md +++ b/docs/en/latest/plugins/serverless.md @@ -92,7 +92,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -123,7 +123,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/ua-restriction.md b/docs/en/latest/plugins/ua-restriction.md index 1087d5a8bc1e..9d847872e356 100644 --- a/docs/en/latest/plugins/ua-restriction.md +++ b/docs/en/latest/plugins/ua-restriction.md @@ -121,7 +121,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/plugins/zipkin.md b/docs/en/latest/plugins/zipkin.md index d1542c69cc59..23aed6992210 100644 --- a/docs/en/latest/plugins/zipkin.md +++ b/docs/en/latest/plugins/zipkin.md @@ -88,7 +88,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -142,7 +142,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/en/latest/router-radixtree.md b/docs/en/latest/router-radixtree.md index 639fcc985ef2..b9f5dea5d7f5 100644 --- a/docs/en/latest/router-radixtree.md +++ b/docs/en/latest/router-radixtree.md @@ -210,7 +210,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -235,7 +235,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -283,7 +283,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/architecture-design/plugin-config.md b/docs/zh/latest/architecture-design/plugin-config.md index 276bf52f5588..4066d03bceda 100644 --- a/docs/zh/latest/architecture-design/plugin-config.md +++ b/docs/zh/latest/architecture-design/plugin-config.md @@ -47,7 +47,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -88,7 +88,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } "plugins": { @@ -115,7 +115,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } "plugins": { diff --git a/docs/zh/latest/architecture-design/route.md b/docs/zh/latest/architecture-design/route.md index 32e85bc1f873..2fb3a8898bc5 100644 --- a/docs/zh/latest/architecture-design/route.md +++ b/docs/zh/latest/architecture-design/route.md @@ -33,7 +33,7 @@ Route 中主要包含三部分内容:匹配规则(比如 uri、host、remote 上面提及重复的缺点在 APISIX 中独立抽象了 [Service](service.md) 和 [Upstream](upstream.md) 两个概念来解决。 -下面创建的 Route 示例,是把 URL 为 "/index.html" 的请求代理到地址为 "39.97.63.215:80" 的 Upstream 服务: +下面创建的 Route 示例,是把 URL 为 "/index.html" 的请求代理到地址为 "127.0.0.1:1980" 的 Upstream 服务: ```shell $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' @@ -42,7 +42,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -54,7 +54,7 @@ Transfer-Encoding: chunked Connection: keep-alive Server: APISIX web server -{"node":{"value":{"uri":"\/index.html","upstream":{"nodes":{"39.97.63.215:80":1},"type":"roundrobin"}},"createdIndex":61925,"key":"\/apisix\/routes\/1","modifiedIndex":61925},"action":"create"} +{"node":{"value":{"uri":"\/index.html","upstream":{"nodes":{"127.0.0.1:1980":1},"type":"roundrobin"}},"createdIndex":61925,"key":"\/apisix\/routes\/1","modifiedIndex":61925},"action":"create"} ``` 当我们接收到成功应答,表示该 Route 已成功创建。 diff --git a/docs/zh/latest/architecture-design/service.md b/docs/zh/latest/architecture-design/service.md index b65750d0916a..154c647955ec 100644 --- a/docs/zh/latest/architecture-design/service.md +++ b/docs/zh/latest/architecture-design/service.md @@ -45,7 +45,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/services/200 -H 'X-API-KEY: edd1c9f034 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/architecture-design/upstream.md b/docs/zh/latest/architecture-design/upstream.md index bf97b8ecbdd1..ca1733edb231 100644 --- a/docs/zh/latest/architecture-design/upstream.md +++ b/docs/zh/latest/architecture-design/upstream.md @@ -75,7 +75,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -97,7 +97,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }, "upstream": { "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } "type": "roundrobin", "retries": 2, diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md index 7e47d8e13d32..e8b5b329a7ca 100644 --- a/docs/zh/latest/plugin-develop.md +++ b/docs/zh/latest/plugin-develop.md @@ -268,7 +268,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/client-control.md b/docs/zh/latest/plugins/client-control.md index 80654f7c1cd5..c1c73d656c72 100644 --- a/docs/zh/latest/plugins/client-control.md +++ b/docs/zh/latest/plugins/client-control.md @@ -57,7 +57,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -92,7 +92,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/ext-plugin-pre-req.md b/docs/zh/latest/plugins/ext-plugin-pre-req.md index 76e9ae6e09fb..b1e18e6b751b 100644 --- a/docs/zh/latest/plugins/ext-plugin-pre-req.md +++ b/docs/zh/latest/plugins/ext-plugin-pre-req.md @@ -60,7 +60,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -87,7 +87,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/hmac-auth.md b/docs/zh/latest/plugins/hmac-auth.md index 282ad1df3ae6..3fa53de82fbc 100644 --- a/docs/zh/latest/plugins/hmac-auth.md +++ b/docs/zh/latest/plugins/hmac-auth.md @@ -87,7 +87,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -322,7 +322,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/ip-restriction.md b/docs/zh/latest/plugins/ip-restriction.md index 30b137ee5fad..be5f89538223 100644 --- a/docs/zh/latest/plugins/ip-restriction.md +++ b/docs/zh/latest/plugins/ip-restriction.md @@ -114,7 +114,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/jwt-auth.md b/docs/zh/latest/plugins/jwt-auth.md index fc0a48bdb2e3..b30ae8384c57 100644 --- a/docs/zh/latest/plugins/jwt-auth.md +++ b/docs/zh/latest/plugins/jwt-auth.md @@ -101,7 +101,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -222,7 +222,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/key-auth.md b/docs/zh/latest/plugins/key-auth.md index 22964c8b69a2..c8a118808167 100644 --- a/docs/zh/latest/plugins/key-auth.md +++ b/docs/zh/latest/plugins/key-auth.md @@ -86,7 +86,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -141,7 +141,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/limit-conn.md b/docs/zh/latest/plugins/limit-conn.md index 1fdaa628dc5a..3470a04bb940 100644 --- a/docs/zh/latest/plugins/limit-conn.md +++ b/docs/zh/latest/plugins/limit-conn.md @@ -60,7 +60,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -87,7 +87,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -127,7 +127,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index 22cde96e335e..a3e9c7aad6c1 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -79,7 +79,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -137,7 +137,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -167,7 +167,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -237,7 +237,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/limit-req.md b/docs/zh/latest/plugins/limit-req.md index 20e0e277675d..204465abdf1e 100644 --- a/docs/zh/latest/plugins/limit-req.md +++ b/docs/zh/latest/plugins/limit-req.md @@ -69,7 +69,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -94,7 +94,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -228,7 +228,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/referer-restriction.md b/docs/zh/latest/plugins/referer-restriction.md index 562c50095152..8232d4f5a914 100644 --- a/docs/zh/latest/plugins/referer-restriction.md +++ b/docs/zh/latest/plugins/referer-restriction.md @@ -110,7 +110,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/serverless.md b/docs/zh/latest/plugins/serverless.md index a1d69ccd5fe7..bb160be377e1 100644 --- a/docs/zh/latest/plugins/serverless.md +++ b/docs/zh/latest/plugins/serverless.md @@ -83,7 +83,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -112,7 +112,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/ua-restriction.md b/docs/zh/latest/plugins/ua-restriction.md index c2a8df9bc40f..e6bd42756943 100644 --- a/docs/zh/latest/plugins/ua-restriction.md +++ b/docs/zh/latest/plugins/ua-restriction.md @@ -116,7 +116,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/zipkin.md b/docs/zh/latest/plugins/zipkin.md index e3134d45d0a8..c9db9c0a7dd4 100644 --- a/docs/zh/latest/plugins/zipkin.md +++ b/docs/zh/latest/plugins/zipkin.md @@ -87,7 +87,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -139,7 +139,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/router-radixtree.md b/docs/zh/latest/router-radixtree.md index 78eb6c670c3b..2bee1cf5dcfa 100644 --- a/docs/zh/latest/router-radixtree.md +++ b/docs/zh/latest/router-radixtree.md @@ -211,7 +211,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -237,7 +237,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' @@ -285,7 +285,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f "upstream": { "type": "roundrobin", "nodes": { - "39.97.63.215:80": 1 + "127.0.0.1:1980": 1 } } }' From 49a539bbb8b97898a468a18b388637df3e7c4878 Mon Sep 17 00:00:00 2001 From: Daming Date: Fri, 26 Nov 2021 11:12:53 +0800 Subject: [PATCH 130/260] feat(http/kafka-logger): support to log response body (#5550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- apisix/plugins/http-logger.lua | 23 +++- apisix/plugins/kafka-logger.lua | 33 ++++-- apisix/utils/log-util.lua | 57 +++++++++- docs/en/latest/plugins/http-logger.md | 4 +- docs/en/latest/plugins/kafka-logger.md | 2 + docs/zh/latest/plugins/http-logger.md | 2 + docs/zh/latest/plugins/kafka-logger.md | 2 + t/plugin/http-logger-json.t | 125 ++++++++++++++++++++- t/plugin/http-logger.t | 36 +++++++ t/plugin/kafka-logger.t | 143 +++++++++++++++++++++++++ 10 files changed, 412 insertions(+), 15 deletions(-) diff --git a/apisix/plugins/http-logger.lua b/apisix/plugins/http-logger.lua index 3d2e7f16a4bf..b52c1adcbae3 100644 --- a/apisix/plugins/http-logger.lua +++ b/apisix/plugins/http-logger.lua @@ -44,6 +44,17 @@ local schema = { inactive_timeout = {type = "integer", minimum = 1, default = 5}, batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false}, + include_resp_body = {type = "boolean", default = false}, + include_resp_body_expr = { + type = "array", + minItems = 1, + items = { + type = "array", + items = { + type = "string" + } + } + }, concat_method = {type = "string", default = "json", enum = {"json", "new_line"}} }, @@ -72,7 +83,12 @@ function _M.check_schema(conf, schema_type) if schema_type == core.schema.TYPE_METADATA then return core.schema.check(metadata_schema, conf) end - return core.schema.check(schema, conf) + + local ok, err = core.schema.check(schema, conf) + if not ok then + return nil, err + end + return log_util.check_log_schema(conf) end @@ -162,6 +178,11 @@ local function remove_stale_objects(premature) end +function _M.body_filter(conf, ctx) + log_util.collect_body(conf, ctx) +end + + function _M.log(conf, ctx) local metadata = plugin.plugin_metadata(plugin_name) core.log.info("metadata: ", core.json.delay_encode(metadata)) diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua index f045c3958e15..def042feb2be 100644 --- a/apisix/plugins/kafka-logger.lua +++ b/apisix/plugins/kafka-logger.lua @@ -19,7 +19,6 @@ local log_util = require("apisix.utils.log-util") local producer = require ("resty.kafka.producer") local batch_processor = require("apisix.utils.batch-processor") local plugin = require("apisix.plugin") -local expr = require("resty.expr.v1") local math = math local pairs = pairs @@ -85,6 +84,17 @@ local schema = { } } }, + include_resp_body = {type = "boolean", default = false}, + include_resp_body_expr = { + type = "array", + minItems = 1, + items = { + type = "array", + items = { + type = "string" + } + } + }, -- in lua-resty-kafka, cluster_name is defined as number -- see https://github.com/doujiang24/lua-resty-kafka#new-1 cluster_name = {type = "integer", minimum = 1, default = 1}, @@ -109,19 +119,15 @@ local _M = { function _M.check_schema(conf, schema_type) - - if conf.include_req_body_expr then - local ok, err = expr.new(conf.include_req_body_expr) - if not ok then - return nil, - {error_msg = "failed to validate the 'include_req_body_expr' expression: " .. err} - end - end - if schema_type == core.schema.TYPE_METADATA then return core.schema.check(metadata_schema, conf) end - return core.schema.check(schema, conf) + + local ok, err = core.schema.check(schema, conf) + if not ok then + return nil, err + end + return log_util.check_log_schema(conf) end @@ -191,6 +197,11 @@ local function send_kafka_data(conf, log_message, prod) end +function _M.body_filter(conf, ctx) + log_util.collect_body(conf, ctx) +end + + function _M.log(conf, ctx) local entry if conf.meta_format == "origin" then diff --git a/apisix/utils/log-util.lua b/apisix/utils/log-util.lua index 3f82b920c26e..3f268aa8c306 100644 --- a/apisix/utils/log-util.lua +++ b/apisix/utils/log-util.lua @@ -123,6 +123,10 @@ local function get_full_log(ngx, conf) latency = (ngx_now() - ngx.req.start_time()) * 1000 } + if ctx.resp_body then + log.response.body = ctx.resp_body + end + if conf.include_req_body then local log_request_body = true @@ -132,7 +136,7 @@ local function get_full_log(ngx, conf) if not conf.request_expr then local request_expr, err = expr.new(conf.include_req_body_expr) if not request_expr then - core.log.error('generate log expr err ' .. err) + core.log.error('generate request expr err ' .. err) return log end conf.request_expr = request_expr @@ -201,6 +205,57 @@ function _M.latency_details_in_ms(ctx) end +function _M.check_log_schema(conf) + if conf.include_req_body_expr then + local ok, err = expr.new(conf.include_req_body_expr) + if not ok then + return nil, "failed to validate the 'include_req_body_expr' expression: " .. err + end + end + if conf.include_resp_body_expr then + local ok, err = expr.new(conf.include_resp_body_expr) + if not ok then + return nil, "failed to validate the 'include_resp_body_expr' expression: " .. err + end + end + return true, nil +end + + +function _M.collect_body(conf, ctx) + if conf.include_resp_body then + local log_response_body = true + + if conf.include_resp_body_expr then + if not conf.response_expr then + local response_expr, err = expr.new(conf.include_resp_body_expr) + if not response_expr then + core.log.error('generate response expr err ' .. err) + return + end + conf.response_expr = response_expr + end + + if ctx.res_expr_eval_result == nil then + ctx.res_expr_eval_result = conf.response_expr:eval(ctx.var) + end + + if not ctx.res_expr_eval_result then + log_response_body = false + end + end + + if log_response_body then + local final_body = core.response.hold_body_chunk(ctx, true) + if not final_body then + return + end + ctx.resp_body = final_body + end + end +end + + function _M.get_rfc3339_zulu_timestamp(timestamp) ngx_update_time() local now = timestamp or ngx_now() diff --git a/docs/en/latest/plugins/http-logger.md b/docs/en/latest/plugins/http-logger.md index acc0bc0ad728..c4f5056aa02e 100644 --- a/docs/en/latest/plugins/http-logger.md +++ b/docs/en/latest/plugins/http-logger.md @@ -49,7 +49,9 @@ This will provide the ability to send Log data requests as JSON objects to Monit | buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed.| | max_retry_count | integer | optional | 0 | [0,...] | Maximum number of retries before removing from the processing pipe line. | | retry_delay | integer | optional | 1 | [0,...] | Number of seconds the process execution should be delayed if the execution fails. | -| include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. | +| include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. Note: if the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitation. | +| include_resp_body| boolean | optional | false | [false, true] | Whether to include the response body. The response body is included if and only if it is `true`. | +| include_resp_body_expr | array | optional | | | When `include_resp_body` is true, control the behavior based on the result of the [lua-resty-expr](https://github.com/api7/lua-resty-expr) expression. If present, only log the response body when the result is true. | | concat_method | string | optional | "json" | ["json", "new_line"] | Enum type: `json` and `new_line`. **json**: use `json.encode` for all pending logs. **new_line**: use `json.encode` for each pending log and concat them with "\n" line. | ## How To Enable diff --git a/docs/en/latest/plugins/kafka-logger.md b/docs/en/latest/plugins/kafka-logger.md index 9aa3d92559a7..504aa116cdc6 100644 --- a/docs/en/latest/plugins/kafka-logger.md +++ b/docs/en/latest/plugins/kafka-logger.md @@ -58,6 +58,8 @@ For more info on Batch-Processor in Apache APISIX please refer. | retry_delay | integer | optional | 1 | [0,...] | Number of seconds the process execution should be delayed if the execution fails. | | include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. Note: if the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitation. | | include_req_body_expr | array | optional | | | When `include_req_body` is true, control the behavior based on the result of the [lua-resty-expr](https://github.com/api7/lua-resty-expr) expression. If present, only log the request body when the result is true. | +| include_resp_body| boolean | optional | false | [false, true] | Whether to include the response body. The response body is included if and only if it is `true`. | +| include_resp_body_expr | array | optional | | | When `include_resp_body` is true, control the behavior based on the result of the [lua-resty-expr](https://github.com/api7/lua-resty-expr) expression. If present, only log the response body when the result is true. | | cluster_name | integer | optional | 1 | [0,...] | the name of the cluster. When there are two or more kafka clusters, you can specify different names. And this only works with async producer_type.| ### examples of meta_format diff --git a/docs/zh/latest/plugins/http-logger.md b/docs/zh/latest/plugins/http-logger.md index 7ea4fd79180b..b253355d4fcf 100644 --- a/docs/zh/latest/plugins/http-logger.md +++ b/docs/zh/latest/plugins/http-logger.md @@ -50,6 +50,8 @@ title: http-logger | max_retry_count | integer | 可选 | 0 | [0,...] | 从处理管道中移除之前的最大重试次数。 | | retry_delay | integer | 可选 | 1 | [0,...] | 如果执行失败,则应延迟执行流程的秒数。 | | include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ; true: 表示包含请求的 body 。 | +| include_resp_body| boolean | 可选 | false | [false, true] | 是否包括响应体。包含响应体,当为`true`。 | +| include_resp_body_expr | array | 可选 | | | 是否采集响体, 基于[lua-resty-expr](https://github.com/api7/lua-resty-expr)。 该选项需要开启 `include_resp_body`| | concat_method | string | 可选 | "json" | ["json", "new_line"] | 枚举类型: `json`、`new_line`。**json**: 对所有待发日志使用 `json.encode` 编码。**new_line**: 对每一条待发日志单独使用 `json.encode` 编码并使用 "\n" 连接起来。 | ## 如何开启 diff --git a/docs/zh/latest/plugins/kafka-logger.md b/docs/zh/latest/plugins/kafka-logger.md index d51158a7c6bf..85955330c646 100644 --- a/docs/zh/latest/plugins/kafka-logger.md +++ b/docs/zh/latest/plugins/kafka-logger.md @@ -58,6 +58,8 @@ title: kafka-logger | retry_delay | integer | 可选 | 1 | [0,...] | 如果执行失败,则应延迟执行流程的秒数。 | | include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ;true: 表示包含请求的 body。注意:如果请求 body 没办法完全放在内存中,由于 Nginx 的限制,我们没有办法把它记录下来。| | include_req_body_expr | array | 可选 | | | 当 `include_req_body` 开启时, 基于 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 表达式的结果进行记录。如果该选项存在,只有在表达式为真的时候才会记录请求 body。 | +| include_resp_body| boolean | 可选 | false | [false, true] | 是否包括响应体。包含响应体,当为`true`。 | +| include_resp_body_expr | array | 可选 | | | 是否采集响体, 基于[lua-resty-expr](https://github.com/api7/lua-resty-expr)。 该选项需要开启 `include_resp_body`| | cluster_name | integer | 可选 | 1 | [0,...] | kafka 集群的名称。当有两个或多个 kafka 集群时,可以指定不同的名称。只适用于 producer_type 是 async 模式。| ### meta_format 参考示例 diff --git a/t/plugin/http-logger-json.t b/t/plugin/http-logger-json.t index ed727c2a7377..9787165532e4 100644 --- a/t/plugin/http-logger-json.t +++ b/t/plugin/http-logger-json.t @@ -42,7 +42,7 @@ run_tests; __DATA__ -=== TEST 1: json body +=== TEST 1: json body with request_body --- apisix_yaml routes: - @@ -62,3 +62,126 @@ POST /hello {"sample_payload":"hello"} --- error_log "body":"{\"sample_payload\":\"hello\"}" + + + +=== TEST 2: json body with response_body +--- apisix_yaml +routes: + - + uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + plugins: + http-logger: + batch_max_size: 1 + uri: http://127.0.0.1:1980/log + include_resp_body: true +#END +--- request +POST /hello +{"sample_payload":"hello"} +--- error_log +"response":{"body":"hello world\n" + + + +=== TEST 3: json body with response_body and response_body expression +--- apisix_yaml +routes: + - + uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + plugins: + http-logger: + batch_max_size: 1 + uri: http://127.0.0.1:1980/log + include_resp_body: true + include_resp_body_expr: + - - arg_bar + - == + - foo +#END +--- request +POST /hello?bar=foo +{"sample_payload":"hello"} +--- error_log +"response":{"body":"hello world\n" + + + +=== TEST 4: json body with response_body, expr not hit +--- apisix_yaml +routes: + - + uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + plugins: + http-logger: + batch_max_size: 1 + uri: http://127.0.0.1:1980/log + include_resp_body: true + include_resp_body_expr: + - - arg_bar + - == + - foo +#END +--- request +POST /hello?bar=bar +{"sample_payload":"hello"} +--- no_error_log +"response":{"body":"hello world\n" + + + +=== TEST 5: json body with request_body and response_body +--- apisix_yaml +routes: + - + uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + plugins: + http-logger: + batch_max_size: 1 + uri: http://127.0.0.1:1980/log + include_req_body: true + include_resp_body: true +#END +--- request +POST /hello +{"sample_payload":"hello"} +--- error_log eval +qr/(.*"response":\{.*"body":"hello world\\n".*|.*\{\\\"sample_payload\\\":\\\"hello\\\"\}.*){2}/ + + + +=== TEST 6: json body without request_body or response_body +--- apisix_yaml +routes: + - + uri: /hello + upstream: + nodes: + "127.0.0.1:1980": 1 + type: roundrobin + plugins: + http-logger: + batch_max_size: 1 + uri: http://127.0.0.1:1980/log +#END +--- request +POST /hello +{"sample_payload":"hello"} +--- error_log eval +qr/(.*"response":\{.*"body":"hello world\\n".*|.*\{\\\"sample_payload\\\":\\\"hello\\\"\}.*){0}/ diff --git a/t/plugin/http-logger.t b/t/plugin/http-logger.t index 1dd012217394..9dd85db18b81 100644 --- a/t/plugin/http-logger.t +++ b/t/plugin/http-logger.t @@ -784,3 +784,39 @@ qr/sending a batch logs to http:\/\/127.0.0.1:1982\/hello\d?/ --- grep_error_log_out sending a batch logs to http://127.0.0.1:1982/hello sending a batch logs to http://127.0.0.1:1982/hello1 + + + +=== TEST 18: check log schema(include_resp_body_expr) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.http-logger") + local ok, err = plugin.check_schema({uri = "http://127.0.0.1", + auth_header = "Basic 123", + timeout = 3, + name = "http-logger", + max_retry_count = 2, + retry_delay = 2, + buffer_duration = 2, + inactive_timeout = 2, + batch_max_size = 500, + include_resp_body = true, + include_resp_body_expr = { + {"bar", "<>", "foo"} + } + }) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +failed to validate the 'include_resp_body_expr' expression: invalid operator '<>' +done +--- no_error_log +[error] diff --git a/t/plugin/kafka-logger.t b/t/plugin/kafka-logger.t index 5094910f37ea..42277c6f1301 100644 --- a/t/plugin/kafka-logger.t +++ b/t/plugin/kafka-logger.t @@ -1193,3 +1193,146 @@ hello world --- no_error_log eval qr/send data to kafka: \{.*"body":"abcdef"/ --- wait: 2 + + + +=== TEST 29: check log schema(include_req_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.kafka-logger") + local ok, err = plugin.check_schema({ + kafka_topic = "test", + key = "key1", + broker_list = { + ["127.0.0.1"] = 3 + }, + include_req_body = true, + include_req_body_expr = { + {"bar", "<>", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +failed to validate the 'include_req_body_expr' expression: invalid operator '<>' +done +--- no_error_log +[error] + + + +=== TEST 30: check log schema(include_resp_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.kafka-logger") + local ok, err = plugin.check_schema({ + kafka_topic = "test", + key = "key1", + broker_list = { + ["127.0.0.1"] = 3 + }, + include_resp_body = true, + include_resp_body_expr = { + {"bar", "", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +failed to validate the 'include_resp_body_expr' expression: invalid operator '' +done +--- no_error_log +[error] + + + +=== TEST 31: set route(id: 1,include_resp_body = true,include_resp_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "kafka-logger": { + "broker_list" : + { + "127.0.0.1":9092 + }, + "kafka_topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_resp_body": true, + "include_resp_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 32: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log eval +qr/send data to kafka: \{.*"body":"hello world\\n"/ +--- wait: 2 + + + +=== TEST 33: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to kafka: \{.*"body":"hello world\\n"/ +--- wait: 2 From 7242216076f1df4225ab5d2d742ed7fb23383306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 26 Nov 2021 19:03:17 +0800 Subject: [PATCH 131/260] feat(google-cloud-logging): set token type based on the Oauth response (#5619) --- apisix/plugins/google-cloud-logging.lua | 2 +- apisix/plugins/google-cloud-logging/oauth.lua | 4 +- t/lib/server.lua | 10 +- t/plugin/google-cloud-logging.t | 133 ++++++++++++++++++ 4 files changed, 144 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/google-cloud-logging.lua b/apisix/plugins/google-cloud-logging.lua index d06d88ed4c8a..f007e9b55b5c 100644 --- a/apisix/plugins/google-cloud-logging.lua +++ b/apisix/plugins/google-cloud-logging.lua @@ -155,7 +155,7 @@ local function send_to_google(oauth, entries) }), headers = { ["Content-Type"] = "application/json", - ["Authorization"] = "Bearer " .. access_token, + ["Authorization"] = (oauth.access_token_type or "Bearer") .. " " .. access_token, }, }) diff --git a/apisix/plugins/google-cloud-logging/oauth.lua b/apisix/plugins/google-cloud-logging/oauth.lua index ebaa2c7adc8e..33f83be6b48a 100644 --- a/apisix/plugins/google-cloud-logging/oauth.lua +++ b/apisix/plugins/google-cloud-logging/oauth.lua @@ -74,8 +74,9 @@ function _M:refresh_access_token() return end - self.access_token_expire_time = get_timestamp() + res.expires_in self.access_token = res.access_token + self.access_token_type = res.token_type + self.access_token_expire_time = get_timestamp() + res.expires_in end @@ -106,6 +107,7 @@ function _M:new(config) auth_uri = config.auth_uri or "https://accounts.google.com/o/oauth2/auth", entries_uri = config.entries_uri or "https://logging.googleapis.com/v2/entries:write", access_token = nil, + access_token_type = nil, access_token_expire_time = 0, } diff --git a/t/lib/server.lua b/t/lib/server.lua index 39320abe5b90..07356850c24b 100644 --- a/t/lib/server.lua +++ b/t/lib/server.lua @@ -444,6 +444,8 @@ function _M._well_known_openid_configuration() end function _M.google_logging_token() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" ngx.req.read_body() local data = ngx.decode_args(ngx.req.get_body_data()) local jwt = require("resty.jwt") @@ -476,11 +478,13 @@ function _M.google_logging_token() ngx.say(json_encode({ access_token = jwt_token, expires_in = expire_time, - token_type = "Bearer" + token_type = args_token_type })) end function _M.google_logging_entries() + local args = ngx.req.get_uri_args() + local args_token_type = args.token_type or "Bearer" ngx.req.read_body() local data = ngx.req.get_body_data() local jwt = require("resty.jwt") @@ -494,7 +498,7 @@ function _M.google_logging_entries() return end - token = string.sub(token, string.len("Bearer") + 2) + token = string.sub(token, string.len(args_token_type) + 2) local verify = jwt:verify(rsa_public_key, token) if not verify.verified then ngx.status = 401 @@ -510,7 +514,7 @@ function _M.google_logging_entries() return end - local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() + local expire_time = (verify.payload.exp or ngx.time()) - ngx.time() if expire_time <= 0 then ngx.status = 403 ngx.say(json_encode({ error = "token has expired" })) diff --git a/t/plugin/google-cloud-logging.t b/t/plugin/google-cloud-logging.t index db1f2deb62cf..1b8a893df5d2 100644 --- a/t/plugin/google-cloud-logging.t +++ b/t/plugin/google-cloud-logging.t @@ -376,3 +376,136 @@ GET /hello --- wait: 2 --- response_body hello world + + + +=== TEST 12: set route (customize auth type) +--- config + location /t { + content_by_lua_block { + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_config = { + private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv +0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7 ++pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL +wQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF +IeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb +2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs +YvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG +-----END RSA PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/logging/token?token_type=Basic", + scopes = { + "https://apisix.apache.org/logs:admin" + }, + entries_uri = "http://127.0.0.1:1980/google/logging/entries?token_type=Basic", + }, + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 13: test route(customize auth type) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world + + + +=== TEST 14: set route (customize auth type error) +--- config + location /t { + content_by_lua_block { + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_config = { + private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv +0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7 ++pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL +wQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF +IeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb +2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs +YvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG +-----END RSA PRIVATE KEY-----]], + project_id = "apisix", + token_uri = "http://127.0.0.1:1980/google/logging/token?token_type=Basic", + scopes = { + "https://apisix.apache.org/logs:admin" + }, + entries_uri = "http://127.0.0.1:1980/google/logging/entries", + }, + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 15: test route(customize auth type error) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world +--- grep_error_log eval +qr/\{\"error\"\:\"[\w+\s+]*\"\}/ +--- grep_error_log_out +{"error":"identity authentication failed"} +--- error_log +Batch Processor[google-cloud-logging] failed to process entries +Batch Processor[google-cloud-logging] exceeded the max_retry_count From 942650037d77007a292b4f80d98975fe9d6a5a37 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Sun, 28 Nov 2021 08:43:54 +0530 Subject: [PATCH 132/260] docs: exec code block for shell commands in Test Nginx (#5625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- docs/en/latest/internal/testing-framework.md | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/en/latest/internal/testing-framework.md b/docs/en/latest/internal/testing-framework.md index db8672952110..7b22fd7b16bc 100644 --- a/docs/en/latest/internal/testing-framework.md +++ b/docs/en/latest/internal/testing-framework.md @@ -286,3 +286,24 @@ ONLY: --- response_body {"action":"get","count":0,"node":{"dir":true,"key":"/apisix/upstreams","nodes":{}}} ``` + +### Executing Shell Commands + +It is possible to execute shell commands while writing tests in test-nginx for APISIX. We expose this feature via `exec` code block. The `stdout` of the executed process can be captured via `response_body` code block and `stderr` (if any) can be captured by filtering error.log through `grep_error_log`. Here is an example: + +``` +=== TEST 1: check exec stdout +--- exec +echo hello world +--- response_body +hello world + + +=== TEST 2: when exec returns an error +--- exec +echxo hello world +--- grep_error_log eval +qr/failed to execute the script [ -~]*/ +--- grep_error_log_out +failed to execute the script with status: 127, reason: exit, stderr: /bin/sh: 1: echxo: not found +``` From 0344606888d8a4253c87ae696a3c45d32a199be2 Mon Sep 17 00:00:00 2001 From: Soham Banerjee <63705023+soham4abc@users.noreply.github.com> Date: Mon, 29 Nov 2021 06:24:36 +0530 Subject: [PATCH 133/260] docs: Path of example is corrected (#5618) --- docs/en/latest/plugins/dubbo-proxy.md | 2 +- docs/zh/latest/plugins/dubbo-proxy.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/plugins/dubbo-proxy.md b/docs/en/latest/plugins/dubbo-proxy.md index 0dc0e8e92cd5..e13767123cb9 100644 --- a/docs/en/latest/plugins/dubbo-proxy.md +++ b/docs/en/latest/plugins/dubbo-proxy.md @@ -71,7 +71,7 @@ Then reload APISIX. Here's an example, enable the dubbo-proxy plugin on the specified route: ```shell -curl http://127.0.0.1:9080/apisix/admin/upstream/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +curl http://127.0.0.1:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "nodes": { "127.0.0.1:20880": 1 diff --git a/docs/zh/latest/plugins/dubbo-proxy.md b/docs/zh/latest/plugins/dubbo-proxy.md index ba23e97daf43..2264672a29b8 100644 --- a/docs/zh/latest/plugins/dubbo-proxy.md +++ b/docs/zh/latest/plugins/dubbo-proxy.md @@ -70,7 +70,7 @@ plugins: 这里有个例子,在指定的路由中启用 `dubbo-proxy` 插件: ```shell -curl http://127.0.0.1:9080/apisix/admin/upstream/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +curl http://127.0.0.1:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "nodes": { "127.0.0.1:20880": 1 From c8c09eb4d98eeda07822e351f43171f75d57e2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 29 Nov 2021 10:22:20 +0800 Subject: [PATCH 134/260] docs: remember to register release info (#5632) --- MAINTAIN.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/MAINTAIN.md b/MAINTAIN.md index 11f1a238e38c..0a0b2cdfc8dc 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -29,11 +29,12 @@ via `VERSION=x.y.z make release-src` 5. Send the [vote email](https://lists.apache.org/thread/vq4qtwqro5zowpdqhx51oznbjy87w9d0) to dev@apisix.apache.org 6. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/k2frnvj4zj9oynsbr7h7nd6n6m3q5p89) to dev@apisix.apache.org 7. Move the vote artifact to Apache's apisix repo -8. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.2) from the minor branch -9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) -10. Update APISIX rpm package -11. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` -12. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org +8. Register the release info in https://reporter.apache.org/addrelease.html?apisix +9. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.2) from the minor branch +10. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) +11. Update APISIX rpm package +12. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` +13. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org ### Release minor version @@ -43,9 +44,10 @@ via `VERSION=x.y.z make release-src` 3. Send the [vote email](https://lists.apache.org/thread/q8zq276o20r5r9qjkg074nfzb77xwry9) to dev@apisix.apache.org 4. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/p1m9s116rojlhb91g38cj8646393qkz7) to dev@apisix.apache.org 5. Move the vote artifact to Apache's apisix repo -6. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.0) from the minor branch -7. Merge the pull request into master branch -8. Update [APISIX's website](https://github.com/apache/apisix-website/commit/7bf0ab5a1bbd795e6571c4bb89a6e646115e7ca3) -9. Update APISIX rpm package -10. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` -11. Send the [ANNOUNCE email](https://lists.apache.org/thread/4s4msqwl1tq13p9dnv3hx7skbgpkozw1) to dev@apisix.apache.org & announce@apache.org +6. Register the release info in https://reporter.apache.org/addrelease.html?apisix +7. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.0) from the minor branch +8. Merge the pull request into master branch +9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/7bf0ab5a1bbd795e6571c4bb89a6e646115e7ca3) +10. Update APISIX rpm package +11. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` +12. Send the [ANNOUNCE email](https://lists.apache.org/thread/4s4msqwl1tq13p9dnv3hx7skbgpkozw1) to dev@apisix.apache.org & announce@apache.org From e7ceda0387909ba41c0eb90a897c7b70e1e96545 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Sun, 28 Nov 2021 20:41:06 -0600 Subject: [PATCH 135/260] feat(ext-plugin): support to get request body (#5600) --- apisix/plugins/ext-plugin/init.lua | 12 ++ t/lib/ext-plugin.lua | 31 ++++- t/plugin/ext-plugin/request-body.t | 201 +++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 t/plugin/ext-plugin/request-body.t diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index 1a8fc9cdb40a..1e3ff0c3da13 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -28,6 +28,7 @@ local extra_info = require("A6.ExtraInfo.Info") local extra_info_req = require("A6.ExtraInfo.Req") local extra_info_var = require("A6.ExtraInfo.Var") local extra_info_resp = require("A6.ExtraInfo.Resp") +local extra_info_reqbody = require("A6.ExtraInfo.ReqBody") local text_entry = require("A6.TextEntry") local err_resp = require("A6.Err.Resp") local err_code = require("A6.Err.Code") @@ -273,6 +274,17 @@ local function handle_extra_info(ctx, input) local var_name = var_req:Name() res = ctx.var[var_name] + elseif info_type == extra_info.ReqBody then + local info = req:Info() + local reqbody_req = extra_info_reqbody.New() + reqbody_req:Init(info.bytes, info.pos) + + local err + res, err = core.request.get_body() + if err then + core.log.error("failed to read request body: ", err) + end + else return nil, "unsupported info type: " .. info_type end diff --git a/t/lib/ext-plugin.lua b/t/lib/ext-plugin.lua index f38951a7f746..33e87d566c3a 100644 --- a/t/lib/ext-plugin.lua +++ b/t/lib/ext-plugin.lua @@ -33,7 +33,7 @@ local extra_info = require("A6.ExtraInfo.Info") local extra_info_req = require("A6.ExtraInfo.Req") local extra_info_var = require("A6.ExtraInfo.Var") local extra_info_resp = require("A6.ExtraInfo.Resp") - +local extra_info_reqbody = require("A6.ExtraInfo.ReqBody") local _M = {} local builder = flatbuffers.Builder(0) @@ -169,6 +169,8 @@ function _M.go(case) local entry = call_req:Args(1) assert(entry:Name() == "x") assert(entry:Value() == "z") + elseif case.get_request_body then + assert(call_req:Method() == a6_method.POST) else assert(call_req:Method() == a6_method.GET) end @@ -208,6 +210,33 @@ function _M.go(case) local res = resp:ResultAsString() assert(res == action.result, res) end + + if action.type == "reqbody" then + extra_info_reqbody.Start(builder) + local reqbody_req = extra_info_reqbody.End(builder) + build_extra_info(reqbody_req, extra_info.ReqBody) + local req = extra_info_req.End(builder) + builder:Finish(req) + data = builder:Output() + local ok, err = ext.send(sock, constants.RPC_EXTRA_INFO, data) + if not ok then + ngx.log(ngx.ERR, err) + return + end + ngx.log(ngx.WARN, "send extra info req successfully") + + local ty, data = ext.receive(sock) + if not ty then + ngx.log(ngx.ERR, data) + return + end + + assert(ty == constants.RPC_EXTRA_INFO, ty) + local buf = flatbuffers.binaryArray.New(data) + local resp = extra_info_resp.GetRootAsResp(buf, 0) + local res = resp:ResultAsString() + assert(res == action.result, res) + end end end diff --git a/t/plugin/ext-plugin/request-body.t b/t/plugin/ext-plugin/request-body.t new file mode 100644 index 000000000000..fe0136241007 --- /dev/null +++ b/t/plugin/ext-plugin/request-body.t @@ -0,0 +1,201 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + $block->set_value("stream_conf_enable", 1); + + if (!defined $block->extra_stream_config) { + my $stream_config = <<_EOC_; + server { + listen unix:\$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + ext.go({}) + } + } + +_EOC_ + $block->set_value("extra_stream_config", $stream_config); + } + + my $unix_socket_path = $ENV{"TEST_NGINX_HTML_DIR"} . "/nginx.sock"; + my $cmd = $block->ext_plugin_cmd // "['sleep', '5s']"; + my $extra_yaml_config = <<_EOC_; +ext-plugin: + path_for_test: $unix_socket_path + cmd: $cmd +_EOC_ + + $block->set_value("extra_yaml_config", $extra_yaml_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: add route +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + + local code, message, res = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "ext-plugin-pre-req": { + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(message) + return + end + + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 2: request body(text) +--- request +POST /hello +123 +--- extra_stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + local actions = { + {type = "reqbody", result = "123"}, + } + ext.go({extra_info = actions, stop = true, get_request_body = true}) + } + } +--- error_code: 405 +--- grep_error_log eval +qr/send extra info req successfully/ +--- grep_error_log_out +send extra info req successfully + + + +=== TEST 3: request body(x-www-form-urlencoded) +--- request +POST /hello +foo=bar +--- more_headers +Content-Type: application/x-www-form-urlencoded +--- extra_stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + local actions = { + {type = "reqbody", result = "foo=bar"}, + } + ext.go({extra_info = actions, stop = true, get_request_body = true}) + } + } +--- error_code: 405 +--- grep_error_log eval +qr/send extra info req successfully/ +--- grep_error_log_out +send extra info req successfully + + + +=== TEST 4: request body(json) +--- request +POST /hello +{"foo":"bar"} +--- more_headers +Content-Type: application/json +--- extra_stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + local actions = { + {type = "reqbody", result = "{\"foo\":\"bar\"}"}, + } + ext.go({extra_info = actions, stop = true, get_request_body = true}) + } + } +--- error_code: 405 +--- grep_error_log eval +qr/send extra info req successfully/ +--- grep_error_log_out +send extra info req successfully + + + +=== TEST 5: request body(nil) +--- request +POST /hello +--- extra_stream_config + server { + + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + local actions = { + {type = "reqbody", result = nil}, + } + ext.go({extra_info = actions, stop = true, get_request_body = true}) + } + } +--- error_code: 405 +--- grep_error_log eval +qr/send extra info req successfully/ +--- grep_error_log_out +send extra info req successfully From 78b3880a5698f5ee345079aebfbb7711b198a45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 29 Nov 2021 11:46:48 +0800 Subject: [PATCH 136/260] feat: release 2.11.0 (#5567) Signed-off-by: spacewander Co-authored-by: litesun <7sunmiao@gmail.com> --- .asf.yaml | 4 ++ .github/workflows/build.yml | 7 --- CHANGELOG.md | 31 ++++++++++ apisix/core/version.lua | 2 +- docs/en/latest/config.json | 2 +- docs/en/latest/how-to-build.md | 14 ++--- docs/zh/latest/CHANGELOG.md | 31 ++++++++++ docs/zh/latest/config.json | 2 +- docs/zh/latest/how-to-build.md | 14 ++--- rockspec/apisix-2.11.0-0.rockspec | 96 +++++++++++++++++++++++++++++++ 10 files changed, 179 insertions(+), 24 deletions(-) create mode 100644 rockspec/apisix-2.11.0-0.rockspec diff --git a/.asf.yaml b/.asf.yaml index 152a7207de4a..0b980d90056d 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -47,6 +47,10 @@ github: required_pull_request_reviews: require_code_owner_reviews: true required_approving_review_count: 2 + release/2.11: + required_pull_request_reviews: + require_code_owner_reviews: true + required_approving_review_count: 2 release/2.10: required_pull_request_reviews: require_code_owner_reviews: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5de3073e6984..825c6ddd323b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,10 +107,3 @@ jobs: - name: Linux Script run: sudo ./ci/${{ matrix.os_name }}_runner.sh script - - - name: Publish Artifact - if: ${{ startsWith(github.ref, 'refs/heads/release/') && matrix.os_name == 'linux_openresty' }} - uses: actions/upload-artifact@v2.2.4 - with: - name: ${{ steps.branch_env.outputs.fullname }} - path: ${{ steps.branch_env.outputs.fullname }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 752cb0e9f806..dc4f86aab636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ title: Changelog ## Table of Contents +- [2.11.0](#2110) - [2.10.2](#2102) - [2.10.1](#2101) - [2.10.0](#2100) @@ -48,6 +49,36 @@ title: Changelog - [0.7.0](#070) - [0.6.0](#060) +## 2.11.0 + +### Change + +- change(wolf-rbac): change default port number and add `authType` parameter to documentation [#5477](https://github.com/apache/apisix/pull/5477) + +### Core + +- :sunrise: feat: support advanced matching based on post form [#5409](https://github.com/apache/apisix/pull/5409) +- :sunrise: feat: initial wasm support [#5288](https://github.com/apache/apisix/pull/5288) +- :sunrise: feat(control): expose services[#5271](https://github.com/apache/apisix/pull/5271) +- :sunrise: feat(control): add dump upstream api [#5259](https://github.com/apache/apisix/pull/5259) +- :sunrise: feat: etcd cluster single node failure APISIX startup failure [#5158](https://github.com/apache/apisix/pull/5158) +- :sunrise: feat: support specify custom sni in etcd conf [#5206](https://github.com/apache/apisix/pull/5206) + +### Plugin + +- :sunrise: feat(plugin): azure serverless functions [#5479](https://github.com/apache/apisix/pull/5479) +- :sunrise: feat(kafka-logger): supports logging request body [#5501](https://github.com/apache/apisix/pull/5501) +- :sunrise: feat: provide skywalking logger plugin [#5478](https://github.com/apache/apisix/pull/5478) +- :sunrise: feat(plugins): Datadog for metrics collection [#5372](https://github.com/apache/apisix/pull/5372) +- :sunrise: feat(limit-* plugin): fallback to remote_addr when key is missing [#5422](https://github.com/apache/apisix/pull/5422) +- :sunrise: feat(limit-count): support multiple variables as key [#5378](https://github.com/apache/apisix/pull/5378) +- :sunrise: feat(limit-conn): support multiple variables as key [#5354](https://github.com/apache/apisix/pull/5354) +- :sunrise: feat(proxy-rewrite): rewrite method [#5292](https://github.com/apache/apisix/pull/5292) +- :sunrise: feat(limit-req): support multiple variables as key [#5302](https://github.com/apache/apisix/pull/5302) +- :sunrise: feat(proxy-cache): support memory-based strategy [#5028](https://github.com/apache/apisix/pull/5028) +- :sunrise: feat(ext-plugin): avoid sending conf request more times [#5183](https://github.com/apache/apisix/pull/5183) +- :sunrise: feat: Add ldap-auth plugin [#3894](https://github.com/apache/apisix/pull/3894) + ## 2.10.2 ### Bugfix diff --git a/apisix/core/version.lua b/apisix/core/version.lua index 1761fb51937a..9ac6caae38e6 100644 --- a/apisix/core/version.lua +++ b/apisix/core/version.lua @@ -15,5 +15,5 @@ -- limitations under the License. -- return { - VERSION = "2.10.2" + VERSION = "2.11.0" } diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 68631c161b88..d40a0ded540d 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -1,5 +1,5 @@ { - "version": "2.10.2", + "version": "2.11.0", "sidebar": [ { "type": "category", diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index ff8ea580198b..2cabe4c22aa8 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -58,7 +58,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep This installation method is suitable for CentOS 7, please run the following command to install Apache APISIX. ```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.2-0.el7.x86_64.rpm +sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.11.0-0.el7.x86_64.rpm ``` ### Installation via Docker @@ -71,16 +71,16 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a ### Installation via Source Release Package -1. Create a directory named `apisix-2.10.2`. +1. Create a directory named `apisix-2.11.0`. ```shell - mkdir apisix-2.10.2 + mkdir apisix-2.11.0 ``` 2. Download Apache APISIX Release source package. ```shell - wget https://downloads.apache.org/apisix/2.10.2/apache-apisix-2.10.2-src.tgz + wget https://downloads.apache.org/apisix/2.11.0/apache-apisix-2.11.0-src.tgz ``` You can also download the Apache APISIX Release source package from the Apache APISIX website. The [Apache APISIX Official Website - Download Page](https://apisix.apache.org/downloads/) also provides source packages for Apache APISIX, APISIX Dashboard and APISIX Ingress Controller. @@ -88,14 +88,14 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a 3. Unzip the Apache APISIX Release source package. ```shell - tar zxvf apache-apisix-2.10.2-src.tgz -C apisix-2.10.2 + tar zxvf apache-apisix-2.11.0-src.tgz -C apisix-2.11.0 ``` 4. Install the runtime dependent Lua libraries. ```shell - # Switch to the apisix-2.10.2 directory - cd apisix-2.10.2 + # Switch to the apisix-2.11.0 directory + cd apisix-2.11.0 # Create dependencies make deps # Install apisix command diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index 031cd233ec22..1de0c071b2e3 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -23,6 +23,7 @@ title: CHANGELOG ## Table of Contents +- [2.11.0](#2110) - [2.10.2](#2102) - [2.10.1](#2101) - [2.10.0](#2100) @@ -48,6 +49,36 @@ title: CHANGELOG - [0.7.0](#070) - [0.6.0](#060) +## 2.11.0 + +### Change + +- wolf-rbac 插件变更默认端口,并在文档中增加 authType 参数 [#5477](https://github.com/apache/apisix/pull/5477) + +### Core + +- :sunrise: 支持基于 POST 表单的高级路由匹配 [#5409](https://github.com/apache/apisix/pull/5409) +- :sunrise: 初步的 WASM 支持 [#5288](https://github.com/apache/apisix/pull/5288) +- :sunrise: control API 暴露 service 配置 [#5271](https://github.com/apache/apisix/pull/5271) +- :sunrise: control API 暴露 upstream 配置 [#5259](https://github.com/apache/apisix/pull/5259) +- :sunrise: 支持在 etcd 少于半数节点不可用时成功启动 [#5158](https://github.com/apache/apisix/pull/5158) +- :sunrise: 支持 etcd 配置里面自定义 SNI [#5206](https://github.com/apache/apisix/pull/5206) + +### Plugin + +- :sunrise: 新增 Azure-functions 插件 [#5479](https://github.com/apache/apisix/pull/5479) +- :sunrise: kafka-logger 支持动态记录请求体 [#5501](https://github.com/apache/apisix/pull/5501) +- :sunrise: 新增 skywalking-logger 插件 [#5478](https://github.com/apache/apisix/pull/5478) +- :sunrise: 新增 datadog 插件 [#5372](https://github.com/apache/apisix/pull/5372) +- :sunrise: limit-* 系列插件,在 key 对应的值不存在时,回退到用客户端地址作为限流的 key [#5422](https://github.com/apache/apisix/pull/5422) +- :sunrise: limit-count 支持使用多个变量作为 key [#5378](https://github.com/apache/apisix/pull/5378) +- :sunrise: limit-conn 支持使用多个变量作为 key [#5354](https://github.com/apache/apisix/pull/5354) +- :sunrise: proxy-rewrite 支持改写 HTTP method [#5292](https://github.com/apache/apisix/pull/5292) +- :sunrise: limit-req 支持使用多个变量作为 key [#5302](https://github.com/apache/apisix/pull/5302) +- :sunrise: proxy-cache 支持基于内存的缓存机制 [#5028](https://github.com/apache/apisix/pull/5028) +- :sunrise: ext-plugin 避免发送重复的 conf 请求 [#5183](https://github.com/apache/apisix/pull/5183) +- :sunrise: 新增 ldap-auth 插件 [#3894](https://github.com/apache/apisix/pull/3894) + ## 2.10.2 ### Bugfix diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 7e4138874650..9a2eef66dd11 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -1,5 +1,5 @@ { - "version": "2.10.2", + "version": "2.11.0", "sidebar": [ { "type": "category", diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index b1116e04858f..520f80d3d8e1 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -58,7 +58,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep 这种安装方式适用于 CentOS 7 操作系统,请运行以下命令安装 Apache APISIX。 ```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.10.2-0.el7.x86_64.rpm +sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.11.0-0.el7.x86_64.rpm ``` ### 通过 Docker 安装 @@ -71,16 +71,16 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2 ### 通过源码包安装 -1. 创建一个名为 `apisix-2.10.2` 的目录。 +1. 创建一个名为 `apisix-2.11.0` 的目录。 ```shell - mkdir apisix-2.10.2 + mkdir apisix-2.11.0 ``` 2. 下载 Apache APISIX Release 源码包: ```shell - wget https://downloads.apache.org/apisix/2.10.2/apache-apisix-2.10.2-src.tgz + wget https://downloads.apache.org/apisix/2.11.0/apache-apisix-2.11.0-src.tgz ``` 您也可以通过 Apache APISIX 官网下载 Apache APISIX Release 源码包。 Apache APISIX 官网也提供了 Apache APISIX、APISIX Dashboard 和 APISIX Ingress Controller 的源码包,详情请参考 [Apache APISIX 官网-下载页](https://apisix.apache.org/zh/downloads)。 @@ -88,14 +88,14 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2 3. 解压 Apache APISIX Release 源码包: ```shell - tar zxvf apache-apisix-2.10.2-src.tgz -C apisix-2.10.2 + tar zxvf apache-apisix-2.11.0-src.tgz -C apisix-2.11.0 ``` 4. 安装运行时依赖的 Lua 库: ```shell - # 切换到 apisix-2.10.2 目录 - cd apisix-2.10.2 + # 切换到 apisix-2.11.0 目录 + cd apisix-2.11.0 # 安装依赖 LUAROCKS_SERVER=https://luarocks.cn make deps # 安装 apisix 命令 diff --git a/rockspec/apisix-2.11.0-0.rockspec b/rockspec/apisix-2.11.0-0.rockspec new file mode 100644 index 000000000000..06ea8ea9b4f1 --- /dev/null +++ b/rockspec/apisix-2.11.0-0.rockspec @@ -0,0 +1,96 @@ +-- +-- 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. +-- + +package = "apisix" +version = "2.11.0-0" +supported_platforms = {"linux", "macosx"} + +source = { + url = "git://github.com/apache/apisix", + branch = "2.11.0", +} + +description = { + summary = "Apache APISIX is a cloud-native microservices API gateway, delivering the ultimate performance, security, open source and scalable platform for all your APIs and microservices.", + homepage = "https://github.com/apache/apisix", + license = "Apache License 2.0", +} + +dependencies = { + "lua-resty-ctxdump = 0.1-0", + "lua-resty-dns-client = 5.2.0", + "lua-resty-template = 2.0", + "lua-resty-etcd = 1.6.0", + "api7-lua-resty-http = 0.2.0", + "lua-resty-balancer = 0.04", + "lua-resty-ngxvar = 0.5.2", + "lua-resty-jit-uuid = 0.0.7", + "lua-resty-healthcheck-api7 = 2.2.0", + "lua-resty-jwt = 0.2.0", + "lua-resty-hmac-ffi = 0.05", + "lua-resty-cookie = 0.1.0", + "lua-resty-session = 2.24", + "opentracing-openresty = 0.1", + "lua-resty-radixtree = 2.8.1", + "lua-protobuf = 0.3.3", + "lua-resty-openidc = 1.7.2-1", + "luafilesystem = 1.7.0-2", + "api7-lua-tinyyaml = 0.3.0", + "nginx-lua-prometheus = 0.20210206", + "jsonschema = 0.9.5", + "lua-resty-ipmatcher = 0.6.1", + "lua-resty-kafka = 0.07", + "lua-resty-logger-socket = 2.0-0", + "skywalking-nginx-lua = 0.5.0", + "base64 = 1.5-2", + "binaryheap = 0.4", + "dkjson = 2.5-2", + "resty-redis-cluster = 1.02-4", + "lua-resty-expr = 1.3.1", + "graphql = 0.0.2", + "argparse = 0.7.1-1", + "luasocket = 3.0rc1-2", + "luasec = 0.9-1", + "lua-resty-consul = 0.3-2", + "penlight = 1.9.2-1", + "ext-plugin-proto = 0.3.0", + "casbin = 1.26.0", + "api7-snowflake = 2.0-1", + "inspect == 3.1.1", + "lualdap = 1.2.6-1", +} + +build = { + type = "make", + build_variables = { + CFLAGS="$(CFLAGS)", + LIBFLAG="$(LIBFLAG)", + LUA_LIBDIR="$(LUA_LIBDIR)", + LUA_BINDIR="$(LUA_BINDIR)", + LUA_INCDIR="$(LUA_INCDIR)", + LUA="$(LUA)", + OPENSSL_INCDIR="$(OPENSSL_INCDIR)", + OPENSSL_LIBDIR="$(OPENSSL_LIBDIR)", + }, + install_variables = { + ENV_INST_PREFIX="$(PREFIX)", + ENV_INST_BINDIR="$(BINDIR)", + ENV_INST_LIBDIR="$(LIBDIR)", + ENV_INST_LUADIR="$(LUADIR)", + ENV_INST_CONFDIR="$(CONFDIR)", + }, +} From 8f0b066c86257ad6af19f9b3b7e209ece95d17c9 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Mon, 29 Nov 2021 11:58:49 +0530 Subject: [PATCH 137/260] feat: faas plugin refactoring with url path forwarding (#5616) --- apisix/plugins/azure-functions.lua | 98 ++----------- .../plugins/serverless/generic-upstream.lua | 135 ++++++++++++++++++ t/plugin/azure-functions.t | 115 +++++++++++++++ 3 files changed, 261 insertions(+), 87 deletions(-) create mode 100644 apisix/plugins/serverless/generic-upstream.lua diff --git a/apisix/plugins/azure-functions.lua b/apisix/plugins/azure-functions.lua index 1597f2aab238..0b0e64d4f050 100644 --- a/apisix/plugins/azure-functions.lua +++ b/apisix/plugins/azure-functions.lua @@ -14,30 +14,15 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -local core = require("apisix.core") -local http = require("resty.http") local plugin = require("apisix.plugin") -local ngx = ngx -local plugin_name = "azure-functions" +local plugin_name, plugin_version, priority = "azure-functions", 0.1, -1900 -local schema = { +local azure_authz_schema = { type = "object", properties = { - function_uri = {type = "string"}, - authorization = { - type = "object", - properties = { - apikey = {type = "string"}, - clientid = {type = "string"} - } - }, - timeout = {type = "integer", minimum = 100, default = 3000}, - ssl_verify = {type = "boolean", default = true}, - keepalive = {type = "boolean", default = true}, - keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, - keepalive_pool = {type = "integer", minimum = 1, default = 5} - }, - required = {"function_uri"} + apikey = {type = "string"}, + clientid = {type = "string"} + } } local metadata_schema = { @@ -48,31 +33,8 @@ local metadata_schema = { } } -local _M = { - version = 0.1, - priority = -1900, - name = plugin_name, - schema = schema, - metadata_schema = metadata_schema -} - -function _M.check_schema(conf, schema_type) - if schema_type == core.schema.TYPE_METADATA then - return core.schema.check(metadata_schema, conf) - end - return core.schema.check(schema, conf) -end - -function _M.access(conf, ctx) - local uri_args = core.request.get_uri_args(ctx) - local headers = core.request.headers(ctx) or {} - local req_body, err = core.request.get_body() - - if err then - core.log.error("error while reading request body: ", err) - return 400 - end - +local function request_processor(conf, ctx, params) + local headers = params.headers or {} -- set authorization headers if not already set by the client -- we are following not to overwrite the authz keys if not headers["x-functions-key"] and @@ -91,47 +53,9 @@ function _M.access(conf, ctx) end end - headers["host"] = nil - local params = { - method = ngx.req.get_method(), - body = req_body, - query = uri_args, - headers = headers, - keepalive = conf.keepalive, - ssl_verify = conf.ssl_verify - } - - -- Keepalive options - if conf.keepalive then - params.keepalive_timeout = conf.keepalive_timeout - params.keepalive_pool = conf.keepalive_pool - end - - local httpc = http.new() - httpc:set_timeout(conf.timeout) - - local res, err = httpc:request_uri(conf.function_uri, params) - - if not res or err then - core.log.error("failed to process azure function, err: ", err) - return 503 - end - - -- According to RFC7540 https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2, endpoint - -- must not generate any connection specific headers for HTTP/2 requests. - local response_headers = res.headers - if ngx.var.http2 then - response_headers["Connection"] = nil - response_headers["Keep-Alive"] = nil - response_headers["Proxy-Connection"] = nil - response_headers["Upgrade"] = nil - response_headers["Transfer-Encoding"] = nil - end - - -- setting response headers - core.response.set_header(response_headers) - - return res.status, res.body + params.headers = headers end -return _M + +return require("apisix.plugins.serverless.generic-upstream")(plugin_name, + plugin_version, priority, request_processor, azure_authz_schema, metadata_schema) diff --git a/apisix/plugins/serverless/generic-upstream.lua b/apisix/plugins/serverless/generic-upstream.lua new file mode 100644 index 000000000000..0ae59b6a7258 --- /dev/null +++ b/apisix/plugins/serverless/generic-upstream.lua @@ -0,0 +1,135 @@ +-- +-- 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 ngx = ngx +local require = require +local type = type +local string = string + +return function(plugin_name, version, priority, request_processor, authz_schema, metadata_schema) + local core = require("apisix.core") + local http = require("resty.http") + local url = require("net.url") + + if request_processor and type(request_processor) ~= "function" then + return "Failed to generate plugin due to invalid header processor type, " .. + "expected: function, received: " .. type(request_processor) + end + + local schema = { + type = "object", + properties = { + function_uri = {type = "string"}, + authorization = authz_schema, + timeout = {type = "integer", minimum = 100, default = 3000}, + ssl_verify = {type = "boolean", default = true}, + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 5} + }, + required = {"function_uri"} + } + + local _M = { + version = version, + priority = priority, + name = plugin_name, + schema = schema, + metadata_schema = metadata_schema + } + + function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + end + return core.schema.check(schema, conf) + end + + function _M.access(conf, ctx) + local uri_args = core.request.get_uri_args(ctx) + local headers = core.request.headers(ctx) or {} + + local req_body, err = core.request.get_body() + + if err then + core.log.error("error while reading request body: ", err) + return 400 + end + + -- forward the url path came through the matched uri + local url_decoded = url.parse(conf.function_uri) + local path = url_decoded.path or "/" + + if ctx.curr_req_matched and ctx.curr_req_matched[":ext"] then + local end_path = ctx.curr_req_matched[":ext"] + + if path:byte(-1) == string.byte("/") or end_path:byte(1) == string.byte("/") then + path = path .. end_path + else + path = path .. "/" .. end_path + end + end + + + headers["host"] = url_decoded.host + local params = { + method = ngx.req.get_method(), + body = req_body, + query = uri_args, + headers = headers, + path = path, + keepalive = conf.keepalive, + ssl_verify = conf.ssl_verify + } + + -- Keepalive options + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + end + + -- modify request info (if required) + request_processor(conf, ctx, params) + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local res, err = httpc:request_uri(conf.function_uri, params) + + if not res or err then + core.log.error("failed to process ", plugin_name, ", err: ", err) + return 503 + end + + -- According to RFC7540 https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2, + -- endpoint must not generate any connection specific headers for HTTP/2 requests. + local response_headers = res.headers + if ngx.var.http2 then + response_headers["Connection"] = nil + response_headers["Keep-Alive"] = nil + response_headers["Proxy-Connection"] = nil + response_headers["Upgrade"] = nil + response_headers["Transfer-Encoding"] = nil + end + + -- setting response headers + core.response.set_header(response_headers) + + return res.status, res.body + end + + return _M +end diff --git a/t/plugin/azure-functions.t b/t/plugin/azure-functions.t index ea4d0649f684..af589136b87e 100644 --- a/t/plugin/azure-functions.t +++ b/t/plugin/azure-functions.t @@ -42,6 +42,24 @@ add_block_preprocessor(sub { } } + location /api { + content_by_lua_block { + ngx.say("invocation /api successful") + } + } + + location /api/httptrigger { + content_by_lua_block { + ngx.say("invocation /api/httptrigger successful") + } + } + + location /api/http/trigger { + content_by_lua_block { + ngx.say("invocation /api/http/trigger successful") + } + } + location /azure-demo { content_by_lua_block { $inside_lua_block @@ -375,3 +393,100 @@ ngx.say("Authz-Header - " .. headers["x-functions-key"] or "") passed passed Authz-Header - metadata_key + + + +=== TEST 10: check if url path being forwarded correctly by creating a semi correct path uri +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- creating a semi path route + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "azure-functions": { + "function_uri": "http://localhost:8765/api" + } + }, + "uri": "/azure/*" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + local code, _, body = t("/azure/httptrigger", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.print(body) + } + } +--- response_body +passed +invocation /api/httptrigger successful + + + +=== TEST 11: check multilevel url path forwarding +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, _, body = t("/azure/http/trigger", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.print(body) + } + } +--- response_body +invocation /api/http/trigger successful + + + +=== TEST 12: check url path forwarding containing multiple slashes +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, _, body = t("/azure///http////trigger", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.print(body) + } + } +--- response_body +invocation /api/http/trigger successful + + + +=== TEST 13: check url path forwarding with no excess path +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, _, body = t("/azure/", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.print(body) + } + } +--- response_body +invocation /api successful From 95af20ac73d3dbd433157e99a462a18529033fb4 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Tue, 30 Nov 2021 09:25:56 +0530 Subject: [PATCH 138/260] refactor: plugins list test representation (#5642) --- t/admin/plugins.t | 79 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/t/admin/plugins.t b/t/admin/plugins.t index f20b8b98ef93..4305b66599bf 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -38,9 +38,82 @@ __DATA__ === TEST 1: get plugins' name --- request -GET /apisix/admin/plugins/list ---- response_body_like eval -qr/\["real-ip","client-control","ext-plugin-pre-req","zipkin","request-id","fault-injection","serverless-pre-function","batch-requests","cors","ip-restriction","ua-restriction","referer-restriction","uri-blocker","request-validation","openid-connect","authz-casbin","wolf-rbac","ldap-auth","hmac-auth","basic-auth","jwt-auth","key-auth","consumer-restriction","authz-keycloak","proxy-mirror","proxy-cache","proxy-rewrite","api-breaker","limit-conn","limit-count","limit-req","gzip","server-info","traffic-split","redirect","response-rewrite","grpc-transcode","prometheus","datadog","echo","http-logger","skywalking-logger","google-cloud-logging","sls-logger","tcp-logger","kafka-logger","syslog","udp-logger","example-plugin","azure-functions","openwhisk","serverless-post-function","ext-plugin-post-req"\]/ +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local json = require('cjson') + local code, _, body = t("/apisix/admin/plugins/list", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local tab = json.decode(body) + for _, v in ipairs(tab) do + ngx.say(v) + end + } + } +--- request +GET /t + +--- response_body +real-ip +client-control +ext-plugin-pre-req +zipkin +request-id +fault-injection +serverless-pre-function +batch-requests +cors +ip-restriction +ua-restriction +referer-restriction +uri-blocker +request-validation +openid-connect +authz-casbin +wolf-rbac +ldap-auth +hmac-auth +basic-auth +jwt-auth +key-auth +consumer-restriction +authz-keycloak +proxy-mirror +proxy-cache +proxy-rewrite +api-breaker +limit-conn +limit-count +limit-req +gzip +server-info +traffic-split +redirect +response-rewrite +grpc-transcode +prometheus +datadog +echo +http-logger +skywalking-logger +google-cloud-logging +sls-logger +tcp-logger +kafka-logger +syslog +udp-logger +example-plugin +azure-functions +openwhisk +serverless-post-function +ext-plugin-post-req + --- no_error_log [error] From 2969a4bf55910dbbb75f44a4ea1779fbf40f68bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 30 Nov 2021 12:30:41 +0800 Subject: [PATCH 139/260] refactor: exact APISIX vars (#5635) --- apisix/core/ctx.lua | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua index 30c764432e0f..f07665ac3e1a 100644 --- a/apisix/core/ctx.lua +++ b/apisix/core/ctx.lua @@ -143,6 +143,16 @@ do var_x_forwarded_proto = true, } + local apisix_var_names = { + route_id = true, + route_name = true, + service_id = true, + service_name = true, + consumer_name = true, + balancer_ip = true, + balancer_port = true, + } + local mt = { __index = function(t, key) local cached = t._cache[key] @@ -205,26 +215,8 @@ do key = sub_str(key, 9) val = get_parsed_graphql()[key] - elseif key == "route_id" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.route_id - - elseif key == "service_id" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.service_id - - elseif key == "consumer_name" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.consumer_name - - elseif key == "route_name" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.route_name - - elseif key == "service_name" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.service_name - - elseif key == "balancer_ip" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.balancer_ip - - elseif key == "balancer_port" then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx.balancer_port + elseif apisix_var_names[key] then + val = ngx.ctx.api_ctx and ngx.ctx.api_ctx[key] else val = get_var(key, t._request) From 50fcd660f5ec00060a22f43b1a91cad7e7b6572a Mon Sep 17 00:00:00 2001 From: litesun Date: Tue, 30 Nov 2021 12:58:36 +0800 Subject: [PATCH 140/260] feat: add gen-vote-content script (#5595) --- Makefile | 1 + utils/gen-vote-contents.sh | 93 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100755 utils/gen-vote-contents.sh diff --git a/Makefile b/Makefile index cc83e7eae1d4..ac08efa00067 100644 --- a/Makefile +++ b/Makefile @@ -363,6 +363,7 @@ release-src: compress-tar mv $(project_release_name).tgz release/$(project_release_name).tgz mv $(project_release_name).tgz.asc release/$(project_release_name).tgz.asc mv $(project_release_name).tgz.sha512 release/$(project_release_name).tgz.sha512 + ./utils/gen-vote-contents.sh $(VERSION) @$(call func_echo_success_status, "$@ -> [ Done ]") diff --git a/utils/gen-vote-contents.sh b/utils/gen-vote-contents.sh new file mode 100755 index 000000000000..cccae1348eca --- /dev/null +++ b/utils/gen-vote-contents.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +# +# 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. +# +VERSION=$1 + +SUBSTRING1=$(echo $VERSION| cut -d'.' -f 1) +SUBSTRING2=$(echo $VERSION| cut -d'.' -f 2) +BLOB_VERSION=$SUBSTRING1.$SUBSTRING2 +CHANGELOG_HASH=$(printf $VERSION | sed 's/\.//g') + +RELEASE_NOTE_PR="https://github.com/apache/apisix/blob/release/$BLOB_VERSION/CHANGELOG.md#$CHANGELOG_HASH" +COMMIT_ID=$(git rev-parse --short HEAD) + +vote_contents=$(cat < ./release/apache-apisix-$VERSION-vote-contents.txt From cc0b601e50fd0d222d80042a1828bc8cdd2674fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 30 Nov 2021 14:19:16 +0800 Subject: [PATCH 141/260] feat(discovery): check schema before starting (#5629) --- apisix/cli/ops.lua | 15 ++++ .../{consul_kv.lua => consul_kv/init.lua} | 86 +----------------- apisix/discovery/consul_kv/schema.lua | 87 +++++++++++++++++++ apisix/discovery/{dns.lua => dns/init.lua} | 25 +----- apisix/discovery/dns/schema.lua | 29 +++++++ .../discovery/{eureka.lua => eureka/init.lua} | 39 +-------- apisix/discovery/eureka/schema.lua | 40 +++++++++ .../discovery/{nacos.lua => nacos/init.lua} | 55 +----------- apisix/discovery/nacos/schema.lua | 57 ++++++++++++ docs/en/latest/discovery.md | 10 ++- docs/zh/latest/discovery.md | 10 ++- t/cli/test_validate_config.sh | 25 ++++++ t/discovery/dns/sanity.t | 36 ++------ 13 files changed, 287 insertions(+), 227 deletions(-) rename apisix/discovery/{consul_kv.lua => consul_kv/init.lua} (85%) create mode 100644 apisix/discovery/consul_kv/schema.lua rename apisix/discovery/{dns.lua => dns/init.lua} (80%) create mode 100644 apisix/discovery/dns/schema.lua rename apisix/discovery/{eureka.lua => eureka/init.lua} (86%) create mode 100644 apisix/discovery/eureka/schema.lua rename apisix/discovery/{nacos.lua => nacos/init.lua} (87%) create mode 100644 apisix/discovery/nacos/schema.lua diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 4c68ef38fadb..3f28e8fedaa7 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -28,7 +28,9 @@ local jsonschema = require("jsonschema") local stderr = io.stderr local ipairs = ipairs local pairs = pairs +local pcall = pcall local print = print +local require = require local type = type local tostring = tostring local tonumber = tonumber @@ -395,6 +397,19 @@ local function init(env) util.die("failed to validate config: ", err, "\n") end + if yaml_conf.discovery then + for kind, conf in pairs(yaml_conf.discovery) do + local ok, schema = pcall(require, "apisix.discovery." .. kind .. ".schema") + if ok then + local validator = jsonschema.generate_validator(schema) + local ok, err = validator(conf) + if not ok then + util.die("invalid discovery ", kind, " configuration: ", err, "\n") + end + end + end + end + -- check the Admin API token local checked_admin_key = false if yaml_conf.apisix.enable_admin and yaml_conf.apisix.allow_admin then diff --git a/apisix/discovery/consul_kv.lua b/apisix/discovery/consul_kv/init.lua similarity index 85% rename from apisix/discovery/consul_kv.lua rename to apisix/discovery/consul_kv/init.lua index 47ec01af0db1..71b72c8d97ea 100644 --- a/apisix/discovery/consul_kv.lua +++ b/apisix/discovery/consul_kv/init.lua @@ -18,6 +18,7 @@ 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 schema = require('apisix.discovery.consul_kv.schema') local resty_consul = require('resty.consul') local cjson = require('cjson') local http = require('resty.http') @@ -48,77 +49,6 @@ local events local events_list local consul_apps -local schema = { - type = "object", - properties = { - servers = { - type = "array", - minItems = 1, - items = { - type = "string", - } - }, - fetch_interval = {type = "integer", minimum = 1, default = 3}, - keepalive = { - type = "boolean", - default = true - }, - prefix = {type = "string", default = "upstreams"}, - 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_keys = { - 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"} -} - local _M = { version = 0.3, } @@ -434,18 +364,8 @@ end function _M.init_worker() local consul_conf = local_conf.discovery.consul_kv - if not consul_conf - or not consul_conf.servers - or #consul_conf.servers == 0 then - error("do not set consul_kv correctly !") - return - end - - local ok, err = core.schema.check(schema, consul_conf) - if not ok then - error("invalid consul_kv configuration: " .. err) - return - end + -- inject the default values + core.schema.check(schema, consul_conf) if consul_conf.dump then local dump = consul_conf.dump diff --git a/apisix/discovery/consul_kv/schema.lua b/apisix/discovery/consul_kv/schema.lua new file mode 100644 index 000000000000..a2ebb5d07919 --- /dev/null +++ b/apisix/discovery/consul_kv/schema.lua @@ -0,0 +1,87 @@ +-- +-- 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 + }, + prefix = {type = "string", default = "upstreams"}, + 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_keys = { + 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/apisix/discovery/dns.lua b/apisix/discovery/dns/init.lua similarity index 80% rename from apisix/discovery/dns.lua rename to apisix/discovery/dns/init.lua index 45d7141d1971..45a894080fa5 100644 --- a/apisix/discovery/dns.lua +++ b/apisix/discovery/dns/init.lua @@ -17,26 +17,12 @@ local core = require("apisix.core") local config_local = require("apisix.core.config_local") +local schema = require('apisix.discovery.dns.schema') local ipairs = ipairs local error = error local dns_client -local schema = { - type = "object", - properties = { - servers = { - type = "array", - minItems = 1, - items = { - type = "string", - }, - }, - }, - required = {"servers"} -} - - local _M = {} @@ -66,13 +52,10 @@ end function _M.init_worker() local local_conf = config_local.local_conf() - local ok, err = core.schema.check(schema, local_conf.discovery.dns) - if not ok then - error("invalid dns discovery configuration: " .. err) - return - end + -- inject the default values + core.schema.check(schema, local_conf.discovery.dns) - local servers = core.table.try_read_attr(local_conf, "discovery", "dns", "servers") + local servers = local_conf.discovery.dns.servers local opts = { hosts = {}, diff --git a/apisix/discovery/dns/schema.lua b/apisix/discovery/dns/schema.lua new file mode 100644 index 000000000000..94fc9c3cbff6 --- /dev/null +++ b/apisix/discovery/dns/schema.lua @@ -0,0 +1,29 @@ +-- +-- 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", + }, + }, + }, + required = {"servers"} +} diff --git a/apisix/discovery/eureka.lua b/apisix/discovery/eureka/init.lua similarity index 86% rename from apisix/discovery/eureka.lua rename to apisix/discovery/eureka/init.lua index 1dd35630a974..79faf9eea571 100644 --- a/apisix/discovery/eureka.lua +++ b/apisix/discovery/eureka/init.lua @@ -18,12 +18,12 @@ local local_conf = require("apisix.core.config_local").local_conf() local http = require("resty.http") local core = require("apisix.core") +local schema = require('apisix.discovery.eureka.schema') local ipmatcher = require("resty.ipmatcher") local ipairs = ipairs local tostring = tostring local type = type local math_random = math.random -local error = error local ngx = ngx local ngx_timer_at = ngx.timer.at local ngx_timer_every = ngx.timer.every @@ -34,31 +34,6 @@ local log = core.log local default_weight local applications -local schema = { - type = "object", - properties = { - host = { - type = "array", - minItems = 1, - items = { - type = "string", - }, - }, - fetch_interval = {type = "integer", minimum = 1, default = 30}, - prefix = {type = "string"}, - weight = {type = "integer", minimum = 0}, - timeout = { - type = "object", - properties = { - connect = {type = "integer", minimum = 1, default = 2000}, - send = {type = "integer", minimum = 1, default = 2000}, - read = {type = "integer", minimum = 1, default = 5000}, - } - }, - }, - required = {"host"} -} - local _M = { version = 0.1, @@ -232,17 +207,9 @@ end function _M.init_worker() - if not local_conf.discovery.eureka or - not local_conf.discovery.eureka.host or #local_conf.discovery.eureka.host == 0 then - error("do not set eureka.host") - return - end + -- inject the default values + core.schema.check(schema, local_conf.discovery.eureka) - local ok, err = core.schema.check(schema, local_conf.discovery.eureka) - if not ok then - error("invalid eureka configuration: " .. err) - return - end default_weight = local_conf.discovery.eureka.weight or 100 log.info("default_weight:", default_weight, ".") local fetch_interval = local_conf.discovery.eureka.fetch_interval or 30 diff --git a/apisix/discovery/eureka/schema.lua b/apisix/discovery/eureka/schema.lua new file mode 100644 index 000000000000..1966b8e32e39 --- /dev/null +++ b/apisix/discovery/eureka/schema.lua @@ -0,0 +1,40 @@ +-- +-- 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 = { + host = { + type = "array", + minItems = 1, + items = { + type = "string", + }, + }, + fetch_interval = {type = "integer", minimum = 1, default = 30}, + prefix = {type = "string"}, + weight = {type = "integer", minimum = 0}, + timeout = { + type = "object", + properties = { + connect = {type = "integer", minimum = 1, default = 2000}, + send = {type = "integer", minimum = 1, default = 2000}, + read = {type = "integer", minimum = 1, default = 5000}, + } + }, + }, + required = {"host"} +} diff --git a/apisix/discovery/nacos.lua b/apisix/discovery/nacos/init.lua similarity index 87% rename from apisix/discovery/nacos.lua rename to apisix/discovery/nacos/init.lua index 663309b7bb95..94a44a2aead7 100644 --- a/apisix/discovery/nacos.lua +++ b/apisix/discovery/nacos/init.lua @@ -19,11 +19,11 @@ local require = require local local_conf = require('apisix.core.config_local').local_conf() local http = require('resty.http') local core = require('apisix.core') +local schema = require('apisix.discovery.nacos.schema') local ipairs = ipairs local type = type local math = math local math_random = math.random -local error = error local ngx = ngx local ngx_re = require('ngx.re') local ngx_timer_at = ngx.timer.at @@ -44,46 +44,6 @@ local default_group_name = "DEFAULT_GROUP" local events local events_list -local host_pattern = [[^http(s)?:\/\/[a-zA-Z0-9-_.:\@%]+$]] -local prefix_pattern = [[^[\/a-zA-Z0-9-_.]+$]] -local schema = { - type = 'object', - properties = { - host = { - type = 'array', - minItems = 1, - items = { - type = 'string', - pattern = host_pattern, - minLength = 2, - maxLength = 100, - }, - }, - fetch_interval = {type = 'integer', minimum = 1, default = 30}, - prefix = { - type = 'string', - pattern = prefix_pattern, - maxLength = 100, - default = '/nacos/v1/' - }, - weight = {type = 'integer', minimum = 1, default = 100}, - timeout = { - type = 'object', - properties = { - connect = {type = 'integer', minimum = 1, default = 2000}, - send = {type = 'integer', minimum = 1, default = 2000}, - read = {type = 'integer', minimum = 1, default = 5000}, - }, - default = { - connect = 2000, - send = 2000, - read = 5000, - } - }, - }, - required = {'host'} -} - local _M = {} @@ -373,17 +333,8 @@ end function _M.init_worker() - if not local_conf.discovery.nacos or - not local_conf.discovery.nacos.host or #local_conf.discovery.nacos.host == 0 then - error('do not set nacos.host') - return - end - - local ok, err = core.schema.check(schema, local_conf.discovery.nacos) - if not ok then - error('invalid nacos configuration: ' .. err) - return - end + -- inject the default values + core.schema.check(schema, local_conf.discovery.nacos) events = require("resty.worker.events") events_list = events.event_list("discovery_nacos_update_application", diff --git a/apisix/discovery/nacos/schema.lua b/apisix/discovery/nacos/schema.lua new file mode 100644 index 000000000000..9816d0789d27 --- /dev/null +++ b/apisix/discovery/nacos/schema.lua @@ -0,0 +1,57 @@ +-- +-- 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 host_pattern = [[^http(s)?:\/\/[a-zA-Z0-9-_.:\@%]+$]] +local prefix_pattern = [[^[\/a-zA-Z0-9-_.]+$]] + + +return { + type = 'object', + properties = { + host = { + type = 'array', + minItems = 1, + items = { + type = 'string', + pattern = host_pattern, + minLength = 2, + maxLength = 100, + }, + }, + fetch_interval = {type = 'integer', minimum = 1, default = 30}, + prefix = { + type = 'string', + pattern = prefix_pattern, + maxLength = 100, + default = '/nacos/v1/' + }, + weight = {type = 'integer', minimum = 1, default = 100}, + timeout = { + type = 'object', + properties = { + connect = {type = 'integer', minimum = 1, default = 2000}, + send = {type = 'integer', minimum = 1, default = 2000}, + read = {type = 'integer', minimum = 1, default = 5000}, + }, + default = { + connect = 2000, + send = 2000, + read = 5000, + } + }, + }, + required = {'host'} +} diff --git a/docs/en/latest/discovery.md b/docs/en/latest/discovery.md index 89f7ef171eff..85adc46f063a 100644 --- a/docs/en/latest/discovery.md +++ b/docs/en/latest/discovery.md @@ -61,11 +61,13 @@ It is very easy for APISIX to extend the discovery client, the basic steps are a ### the example of Eureka -#### Implementation of eureka.lua +#### Implementation of Eureka client -First, add [`eureka.lua`](../../../apisix/discovery/eureka.lua) in the `apisix/discovery/` directory; +First, create a directory `eureka` under `apisix/discovery`; -Then implement the `_M.init_worker()` function for initialization and the `_M.nodes(service_name)` function for obtaining the list of service instance nodes in `eureka.lua`: +After that, add [`init.lua`](../../../apisix/discovery/eureka/init.lua) in the `apisix/discovery/eureka` directory; + +Then implement the `_M.init_worker()` function for initialization and the `_M.nodes(service_name)` function for obtaining the list of service instance nodes in `init.lua`: ```lua local _M = { @@ -91,6 +93,8 @@ Then implement the `_M.init_worker()` function for initialization and the `_M.no return _M ``` +Finally, provide the schema for YAML configuration in the `schema.lua` under `apisix/discovery/eureka`. + #### How convert Eureka's instance data to APISIX's node? Here's an example of Eureka's data: diff --git a/docs/zh/latest/discovery.md b/docs/zh/latest/discovery.md index c5cfda7c765e..5bad172850f5 100644 --- a/docs/zh/latest/discovery.md +++ b/docs/zh/latest/discovery.md @@ -69,11 +69,13 @@ APISIX 要扩展注册中心其实是件非常容易的事情,其基本步骤 ### 以 Eureka 举例 -#### 实现 eureka.lua +#### 实现 eureka 客户端 -首先在 `apisix/discovery/` 目录中添加 [`eureka.lua`](../../../apisix/discovery/eureka.lua); +首先,在 `apisix/discovery` 下创建 `eureka` 目录; -然后在 `eureka.lua` 实现用于初始化的 `init_worker` 函数以及用于获取服务实例节点列表的 `nodes` 函数即可: +其次,在 `apisix/discovery/eureka` 目录中添加 [`init.lua`](../../../apisix/discovery/eureka/init.lua); + +然后在 `init.lua` 实现用于初始化的 `init_worker` 函数以及用于获取服务实例节点列表的 `nodes` 函数即可: ```lua local _M = { @@ -94,6 +96,8 @@ end return _M ``` +最后,在 `apisix/discovery/eureka` 下的 `schema.lua` 里面提供 YAML 配置的 schema。 + #### Eureka 与 APISIX 之间数据转换逻辑 APISIX 是通过 `upstream.nodes` 来配置上游服务的,所以使用注册中心后,通过注册中心获取服务的所有 node 后,赋值给 `upstream.nodes` 来达到相同的效果。那么 APISIX 是怎么将 Eureka 的数据转成 node 的呢? 假如从 Eureka 获取如下数据: diff --git a/t/cli/test_validate_config.sh b/t/cli/test_validate_config.sh index 8b562d236915..42cd2be4f10c 100755 --- a/t/cli/test_validate_config.sh +++ b/t/cli/test_validate_config.sh @@ -21,6 +21,31 @@ . ./t/cli/common.sh +echo ' +discovery: + nacos: + host: "127.0.0.1" +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'property "host" validation failed: wrong type: expected array, got string'; then + echo "failed: should check discovery schema during init" + exit 1 +fi + +echo ' +discovery: + unknown: + host: "127.0.0.1" +' > conf/config.yaml + +if ! make init; then + echo "failed: should ignore discovery without schema" + exit 1 +fi + +echo "passed: check discovery schema during init" + echo ' apisix: dns_resolver_valid: "/apisix" diff --git a/t/discovery/dns/sanity.t b/t/discovery/dns/sanity.t index 4aec6483e71c..89c864317ceb 100644 --- a/t/discovery/dns/sanity.t +++ b/t/discovery/dns/sanity.t @@ -166,29 +166,7 @@ failed to query the DNS server -=== TEST 7: bad discovery configuration ---- yaml_config -apisix: - node_listen: 1984 - config_center: yaml - enable_admin: false - enable_resolv_search_option: false -discovery: # service discovery center - dns: - servers: "127.0.0.1:1053" ---- apisix_yaml -upstreams: - - service_name: apisix - discovery_type: dns - type: roundrobin - id: 1 ---- error_log -invalid dns discovery configuration ---- error_code: 500 - - - -=== TEST 8: SRV +=== TEST 7: SRV --- apisix_yaml upstreams: - service_name: "srv.test.local" @@ -204,7 +182,7 @@ hello world -=== TEST 9: SRV (RFC 2782 style) +=== TEST 8: SRV (RFC 2782 style) --- apisix_yaml upstreams: - service_name: "_sip._tcp.srv.test.local" @@ -220,7 +198,7 @@ hello world -=== TEST 10: SRV (different port) +=== TEST 9: SRV (different port) --- apisix_yaml upstreams: - service_name: "port.srv.test.local" @@ -236,7 +214,7 @@ hello world -=== TEST 11: SRV (zero weight) +=== TEST 10: SRV (zero weight) --- apisix_yaml upstreams: - service_name: "zero-weight.srv.test.local" @@ -252,7 +230,7 @@ hello world -=== TEST 12: SRV (split weight) +=== TEST 11: SRV (split weight) --- apisix_yaml upstreams: - service_name: "split-weight.srv.test.local" @@ -268,7 +246,7 @@ hello world -=== TEST 13: SRV (priority) +=== TEST 12: SRV (priority) --- apisix_yaml upstreams: - service_name: "priority.srv.test.local" @@ -287,7 +265,7 @@ proxy request to 127.0.0.2:1980 -=== TEST 14: prefer SRV than A +=== TEST 13: prefer SRV than A --- apisix_yaml upstreams: - service_name: "srv-a.test.local" From 150c29d260390a3e7e43e27b554b311b3ebffebf Mon Sep 17 00:00:00 2001 From: MizuhaHimuraki Date: Tue, 30 Nov 2021 17:59:05 +0800 Subject: [PATCH 142/260] feat: report error when use etcd client cert auth (#5640) --- apisix/cli/etcd.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apisix/cli/etcd.lua b/apisix/cli/etcd.lua index 4595ec5938a5..53c41468cf90 100644 --- a/apisix/cli/etcd.lua +++ b/apisix/cli/etcd.lua @@ -347,6 +347,13 @@ function _M.init(env, args) util.die(errmsg) end + if res_put:find("CommonName of client sending a request against gateway", 1, true) then + errmsg = str_format("etcd \"client-cert-auth\" cannot be used with gRPC-gateway, " + .. "please configure the etcd username and password " + .. "in configuration file\n") + util.die(errmsg) + end + if res_put:find("error", 1, true) then is_success = false if (index == host_count) then From df0d7ad5aef101e0b4dc3d888703d965c29d6bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Dec 2021 08:51:42 +0800 Subject: [PATCH 143/260] chore(deps): bump actions/setup-node from 2.4.1 to 2.5.0 (#5644) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/doc-lint.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 873a19afc010..685077b8880f 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2.4.0 - name: 🚀 Use Node.js - uses: actions/setup-node@v2.4.1 + uses: actions/setup-node@v2.5.0 with: node-version: '12.x' - run: npm install -g markdownlint-cli@0.25.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1e4c9bb2a0e3..9b9ac51e58aa 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Setup Nodejs env - uses: actions/setup-node@v2 + uses: actions/setup-node@v2.5.0 with: node-version: '12' From 019eca71b31de632541d9f1fe41666f9531d394b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Wed, 1 Dec 2021 08:52:42 +0800 Subject: [PATCH 144/260] fix: google cloud logging plugin file config error (#5646) --- apisix/plugins/google-cloud-logging.lua | 4 +- t/plugin/google-cloud-logging.t | 96 +++++++++++++++++++++++ t/plugin/google-cloud-logging/config.json | 9 +++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 t/plugin/google-cloud-logging/config.json diff --git a/apisix/plugins/google-cloud-logging.lua b/apisix/plugins/google-cloud-logging.lua index f007e9b55b5c..961762d2e27f 100644 --- a/apisix/plugins/google-cloud-logging.lua +++ b/apisix/plugins/google-cloud-logging.lua @@ -187,7 +187,7 @@ local function get_auth_config(config) local file_content, err = core.io.get_file(config.auth_file) if not file_content then - return nil, "failed to read configuration, file: " .. config.auth_file .. " err:" .. err + return nil, "failed to read configuration, file: " .. config.auth_file .. " err: " .. err end local config_data @@ -196,7 +196,7 @@ local function get_auth_config(config) return nil, "config parse failure, data: " .. file_content .. " , err: " .. err end - auth_config_cache = config.auth_config + auth_config_cache = config_data return auth_config_cache end diff --git a/t/plugin/google-cloud-logging.t b/t/plugin/google-cloud-logging.t index 1b8a893df5d2..202e28c4398e 100644 --- a/t/plugin/google-cloud-logging.t +++ b/t/plugin/google-cloud-logging.t @@ -509,3 +509,99 @@ qr/\{\"error\"\:\"[\w+\s+]*\"\}/ --- error_log Batch Processor[google-cloud-logging] failed to process entries Batch Processor[google-cloud-logging] exceeded the max_retry_count + + + +=== TEST 16: set route (file configuration is successful) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_file = "t/plugin/google-cloud-logging/config.json", + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 17: test route(file configuration is successful) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world + + + +=== TEST 18: set route (file configuration is failed) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_file = "google-cloud-logging/config.json", + inactive_timeout = 1, + batch_max_size = 1, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 19: test route(file configuration is failed) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world +--- error_log +config.json: No such file or directory diff --git a/t/plugin/google-cloud-logging/config.json b/t/plugin/google-cloud-logging/config.json new file mode 100644 index 000000000000..015dab2f1321 --- /dev/null +++ b/t/plugin/google-cloud-logging/config.json @@ -0,0 +1,9 @@ +{ + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv\n0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7\n+pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL\nwQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF\nIeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb\n2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs\nYvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG\n-----END RSA PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "http://127.0.0.1:1980/google/logging/token", + "scopes": [ + "https://apisix.apache.org/logs:admin" + ], + "entries_uri": "http://127.0.0.1:1980/google/logging/entries" +} From b4921c3a1919a7722b54393ed7c7e81825ca9d2c Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 1 Dec 2021 07:19:30 +0530 Subject: [PATCH 145/260] docs: minor update in azure faas http2 documentation (#5641) --- docs/en/latest/plugins/azure-functions.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/latest/plugins/azure-functions.md b/docs/en/latest/plugins/azure-functions.md index a999a50fb3e2..1f9bdd7ad9a0 100644 --- a/docs/en/latest/plugins/azure-functions.md +++ b/docs/en/latest/plugins/azure-functions.md @@ -74,7 +74,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/azure-functions -H 'X- ## How To Enable -The following is an example of how to enable the azure-function faas plugin for a specific route URI. We are assuming your cloud function is already up and running. +The following is an example of how to enable the azure-function faas plugin for a specific APISIX route URI. We are assuming your cloud function is already up and running. ```shell # enable azure function for a route @@ -95,7 +95,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 Now any requests (HTTP/1.1, HTTPS, HTTP2) to URI `/azure` will trigger an HTTP invocation to the aforesaid function URI and response body along with the response headers and response code will be proxied back to the client. For example ( here azure cloud function just take the `name` query param and returns `Hello $name` ) : ```shell -$ curl -i -XGET http://localhost:9080/azure\?name=apisix +$ curl -i -XGET http://localhost:9080/azure\?name=APISIX HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked @@ -104,20 +104,20 @@ Request-Context: appId=cid-v1:38aae829-293b-43c2-82c6-fa94aec0a071 Date: Wed, 17 Nov 2021 14:46:55 GMT Server: APISIX/2.10.2 -Hello, apisix +Hello, APISIX ``` -For requests where the mode of communication between the client and the Apache APISIX gateway is HTTP/2, the example looks like ( make sure you are running APISIX agent with `enable_http2: true` for a port in conf.yaml or uncomment port 9081 of `node_listen` field inside [config-default.yaml](../../../../conf/config-default.yaml) ) : +For requests where the mode of communication between the client and the Apache APISIX gateway is HTTP/2, the example looks like ( make sure you are running APISIX agent with `enable_http2: true` for a port in `config-default.yaml`. You can do it by uncommenting the port 9081 from `apisix.node_listen` field ) : ```shell -$ curl -i -XGET --http2 --http2-prior-knowledge http://localhost:9081/azure\?name=apisix +$ curl -i -XGET --http2 --http2-prior-knowledge http://localhost:9081/azure\?name=APISIX HTTP/2 200 content-type: text/plain; charset=utf-8 request-context: appId=cid-v1:38aae829-293b-43c2-82c6-fa94aec0a071 date: Wed, 17 Nov 2021 14:54:07 GMT server: APISIX/2.10.2 -Hello, apisix +Hello, APISIX ``` ## Disable Plugin From e90e3b7aa1d1789999b3ac740ebf6fcf887ff951 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 1 Dec 2021 08:09:45 +0530 Subject: [PATCH 146/260] feat(plugins): aws lambda serverless (#5594) --- apisix/plugins/aws-lambda.lua | 183 ++++++++++++++++ conf/config-default.yaml | 1 + docs/en/latest/config.json | 3 +- docs/en/latest/plugins/aws-lambda.md | 156 ++++++++++++++ t/admin/plugins.t | 1 + t/plugin/aws-lambda.t | 299 +++++++++++++++++++++++++++ 6 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 apisix/plugins/aws-lambda.lua create mode 100644 docs/en/latest/plugins/aws-lambda.md create mode 100644 t/plugin/aws-lambda.t diff --git a/apisix/plugins/aws-lambda.lua b/apisix/plugins/aws-lambda.lua new file mode 100644 index 000000000000..fe4d7f394cd2 --- /dev/null +++ b/apisix/plugins/aws-lambda.lua @@ -0,0 +1,183 @@ +-- +-- 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 ngx = ngx +local hmac = require("resty.hmac") +local hex_encode = require("resty.string").to_hex +local resty_sha256 = require("resty.sha256") +local str_strip = require("pl.stringx").strip +local norm_path = require("pl.path").normpath +local pairs = pairs +local tab_concat = table.concat +local tab_sort = table.sort +local os = os + + +local plugin_name = "aws-lambda" +local plugin_version = 0.1 +local priority = -1899 + +local ALGO = "AWS4-HMAC-SHA256" + +local function hmac256(key, msg) + return hmac:new(key, hmac.ALGOS.SHA256):final(msg) +end + +local function sha256(msg) + local hash = resty_sha256:new() + hash:update(msg) + local digest = hash:final() + return hex_encode(digest) +end + +local function get_signature_key(key, datestamp, region, service) + local kDate = hmac256("AWS4" .. key, datestamp) + local kRegion = hmac256(kDate, region) + local kService = hmac256(kRegion, service) + local kSigning = hmac256(kService, "aws4_request") + return kSigning +end + +local aws_authz_schema = { + type = "object", + properties = { + -- API Key based authorization + apikey = {type = "string"}, + -- IAM role based authorization, works via aws v4 request signing + -- more at https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html + iam = { + type = "object", + properties = { + accesskey = { + type = "string", + description = "access key id from from aws iam console" + }, + secretkey = { + type = "string", + description = "secret access key from from aws iam console" + }, + aws_region = { + type = "string", + default = "us-east-1", + description = "the aws region that is receiving the request" + }, + service = { + type = "string", + default = "execute-api", + description = "the service that is receiving the request" + } + }, + required = {"accesskey", "secretkey"} + } + } +} + +local function request_processor(conf, ctx, params) + local headers = params.headers + -- set authorization headers if not already set by the client + -- we are following not to overwrite the authz keys + if not headers["x-api-key"] then + if conf.authorization and conf.authorization.apikey then + headers["x-api-key"] = conf.authorization.apikey + return + end + end + + -- performing aws v4 request signing for IAM authorization + -- visit https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html + -- to look at the pseudocode in python. + if headers["authorization"] or not conf.authorization or not conf.authorization.iam then + return + end + + -- create a date for headers and the credential string + local t = ngx.time() + local amzdate = os.date("!%Y%m%dT%H%M%SZ", t) + local datestamp = os.date("!%Y%m%d", t) -- Date w/o time, used in credential scope + headers["X-Amz-Date"] = amzdate + + -- computing canonical uri + local canonical_uri = norm_path(params.path) + if canonical_uri ~= "/" then + if canonical_uri:sub(-1, -1) == "/" then + canonical_uri = canonical_uri:sub(1, -2) + end + if canonical_uri:sub(1, 1) ~= "/" then + canonical_uri = "/" .. canonical_uri + end + end + + -- computing canonical query string + local canonical_qs = {} + for k, v in pairs(params.query) do + canonical_qs[#canonical_qs+1] = ngx.unescape_uri(k) .. "=" .. ngx.unescape_uri(v) + end + + tab_sort(canonical_qs) + canonical_qs = tab_concat(canonical_qs, "&") + + -- computing canonical and signed headers + + local canonical_headers, signed_headers = {}, {} + for k, v in pairs(headers) do + k = k:lower() + if k ~= "connection" then + signed_headers[#signed_headers+1] = k + -- strip starting and trailing spaces including strip multiple spaces into single space + canonical_headers[k] = str_strip(v) + end + end + tab_sort(signed_headers) + + for i = 1, #signed_headers do + local k = signed_headers[i] + canonical_headers[i] = k .. ":" .. canonical_headers[k] .. "\n" + end + canonical_headers = tab_concat(canonical_headers, nil, 1, #signed_headers) + signed_headers = tab_concat(signed_headers, ";") + + -- combining elements to form the canonical request (step-1) + local canonical_request = params.method:upper() .. "\n" + .. canonical_uri .. "\n" + .. (canonical_qs or "") .. "\n" + .. canonical_headers .. "\n" + .. signed_headers .. "\n" + .. sha256(params.body or "") + + -- creating the string to sign for aws signature v4 (step-2) + local iam = conf.authorization.iam + local credential_scope = datestamp .. "/" .. iam.aws_region .. "/" + .. iam.service .. "/aws4_request" + local string_to_sign = ALGO .. "\n" + .. amzdate .. "\n" + .. credential_scope .. "\n" + .. sha256(canonical_request) + + -- calculate the signature (step-3) + local signature_key = get_signature_key(iam.secretkey, datestamp, iam.aws_region, iam.service) + local signature = hex_encode(hmac256(signature_key, string_to_sign)) + + -- add info to the headers (step-4) + headers["authorization"] = ALGO .. " Credential=" .. iam.accesskey + .. "/" .. credential_scope + .. ", SignedHeaders=" .. signed_headers + .. ", Signature=" .. signature +end + + +local serverless_obj = require("apisix.plugins.serverless.generic-upstream") + +return serverless_obj(plugin_name, plugin_version, priority, request_processor, aws_authz_schema) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index dc42e04bea5c..7cf130b256cb 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -355,6 +355,7 @@ plugins: # plugin list (sorted by priority) # <- recommend to use priority (0, 100) for your custom plugins - example-plugin # priority: 0 #- skywalking # priority: -1100 + - aws-lambda # priority: -1899 - azure-functions # priority: -1900 - openwhisk # priority: -1901 - serverless-post-function # priority: -2000 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index d40a0ded540d..8a775e099b29 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -130,7 +130,8 @@ "items": [ "plugins/serverless", "plugins/azure-functions", - "plugins/openwhisk" + "plugins/openwhisk", + "plugins/aws-lambda" ] }, { diff --git a/docs/en/latest/plugins/aws-lambda.md b/docs/en/latest/plugins/aws-lambda.md new file mode 100644 index 000000000000..5afb805a1071 --- /dev/null +++ b/docs/en/latest/plugins/aws-lambda.md @@ -0,0 +1,156 @@ +--- +title: aws-lambda +--- + + + +## Summary + +- [Summary](#summary) +- [Name](#name) +- [Attributes](#attributes) + - [IAM Authorization Schema](#iam-authorization-schema) +- [How To Enable](#how-to-enable) +- [Disable Plugin](#disable-plugin) + +## Name + +`aws-lambda` is a serverless plugin built into Apache APISIX for seamless integration with [AWS Lambda](https://aws.amazon.com/lambda/), a widely used serverless solution, as a dynamic upstream to proxy all requests for a particular URI to the AWS cloud - one of the highly used public cloud platforms for production environment. If enabled, this plugin terminates the ongoing request to that particular URI and initiates a new request to the AWS lambda gateway uri (the new upstream) on behalf of the client with the suitable authorization details set by the users, request headers, request body, params (all these three components are passed from the original request) and returns the response body, status code and the headers back to the original client that has invoked the request to the APISIX agent. +At present, the plugin supports authorization via AWS api key and AWS IAM Secrets. + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | +| function_uri | string | required | | | The AWS api gateway endpoint which triggers the lambda serverless function code. | +| authorization | object | optional | | | Authorization credentials to access the cloud function. | +| authorization.apikey | string | optional | | | Field inside _authorization_. The generate API Key to authorize requests to that endpoint of the AWS gateway. | | +| authorization.iam | object | optional | | | Field inside _authorization_. AWS IAM role based authorization, performed via AWS v4 request signing. See schema details below ([here](#iam-authorization-schema)). | | +| timeout | integer | optional | 3000 | [100,...] | Proxy request timeout in milliseconds. | +| ssl_verify | boolean | optional | true | true/false | If enabled performs SSL verification of the server. | +| keepalive | boolean | optional | true | true/false | To reuse the same proxy connection in near future. Set to false to disable keepalives and immediately close the connection. | +| keepalive_pool | integer | optional | 5 | [1,...] | The maximum number of connections in the pool. | +| keepalive_timeout | integer | optional | 60000 | [1000,...] | The maximal idle timeout (ms). | + +### IAM Authorization Schema + +| Name | Type | Requirement | Default | Valid | Description | +| ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | +| accesskey | string | required | | | Generated access key ID from AWS IAM console. | +| secret_key | string | required | | | Generated access key secret from AWS IAM console. | +| aws_region | string | optional | "us-east-1" | | The AWS region where the request is being sent. | +| service | string | optional | "execute-api" | | The service that is receiving the request (In case of Http Trigger it is "execute-api"). | + +## How To Enable + +The following is an example of how to enable the aws-lambda faas plugin for a specific route URI. Calling the APISIX route uri will make an invocation to the lambda function uri (the new upstream). We are assuming your cloud function is already up and running. + +```shell +# enable aws lambda for a route via api key authorization +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "aws-lambda": { + "function_uri": "https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com/default/test-apisix", + "authorization": { + "apikey": "", + }, + "ssl_verify":false + } + }, + "uri": "/aws" +}' +``` + +Now any requests (HTTP/1.1, HTTPS, HTTP2) to URI `/aws` will trigger an HTTP invocation to the aforesaid function URI and response body along with the response headers and response code will be proxied back to the client. For example (here AWS lambda function just take the `name` query param and returns `Hello $name`) : + +```shell +$ curl -i -XGET localhost:9080/aws\?name=APISIX +HTTP/1.1 200 OK +Content-Type: application/json +Connection: keep-alive +Date: Sat, 27 Nov 2021 13:08:27 GMT +x-amz-apigw-id: JdwXuEVxIAMFtKw= +x-amzn-RequestId: 471289ab-d3b7-4819-9e1a-cb59cac611e0 +Content-Length: 16 +X-Amzn-Trace-Id: Root=1-61a22dca-600c552d1c05fec747fd6db0;Sampled=0 +Server: APISIX/2.10.2 + +"Hello, APISIX!" +``` + +For requests where the mode of communication between the client and the Apache APISIX gateway is HTTP/2, the example looks like ( make sure you are running APISIX agent with `enable_http2: true` for a port in `config-default.yaml`. You can do it by uncommenting the port 9081 from `apisix.node_listen` field ) : + +```shell +$ curl -i -XGET --http2 --http2-prior-knowledge localhost:9081/aws\?name=APISIX +HTTP/2 200 +content-type: application/json +content-length: 16 +x-amz-apigw-id: JdwulHHrIAMFoFg= +date: Sat, 27 Nov 2021 13:10:53 GMT +x-amzn-trace-id: Root=1-61a22e5d-342eb64077dc9877644860dd;Sampled=0 +x-amzn-requestid: a2c2b799-ecc6-44ec-b586-38c0e3b11fe4 +server: APISIX/2.10.2 + +"Hello, APISIX!" +``` + +Similarly, the lambda can be triggered via AWS API Gateway by using AWS `IAM` permissions to authorize access to your API via APISIX aws-lambda plugin. Plugin includes authentication signatures in their HTTP calls via AWS v4 request signing. Here is an example: + +```shell +# enable aws lambda for a route via iam authorization +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "aws-lambda": { + "function_uri": "https://ajycz5e0v9.execute-api.us-east-1.amazonaws.com/default/test-apisix", + "authorization": { + "iam": { + "accesskey": "", + "secretkey": "" + } + }, + "ssl_verify": false + } + }, + "uri": "/aws" +}' +``` + +**Note**: This approach assumes you already have an iam user with the programmatic access enabled and required permissions (`AmazonAPIGatewayInvokeFullAccess`) to access the endpoint. + +## Disable Plugin + +Remove the corresponding JSON configuration in the plugin configuration to disable the `aws-lambda` plugin and add the suitable upstream configuration. +APISIX plugins are hot-reloaded, therefore no need to restart APISIX. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/aws", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 4305b66599bf..dbe585997b08 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -109,6 +109,7 @@ kafka-logger syslog udp-logger example-plugin +aws-lambda azure-functions openwhisk serverless-post-function diff --git a/t/plugin/aws-lambda.t b/t/plugin/aws-lambda.t new file mode 100644 index 000000000000..78a114900e56 --- /dev/null +++ b/t/plugin/aws-lambda.t @@ -0,0 +1,299 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $inside_lua_block = $block->inside_lua_block // ""; + chomp($inside_lua_block); + my $http_config = $block->http_config // <<_EOC_; + + server { + listen 8765; + + location /httptrigger { + content_by_lua_block { + ngx.req.read_body() + local msg = "aws lambda invoked" + ngx.header['Content-Length'] = #msg + 1 + ngx.header['Connection'] = "Keep-Alive" + ngx.say(msg) + } + } + + location /generic { + content_by_lua_block { + $inside_lua_block + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: checking iam schema +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.aws-lambda") + local ok, err = plugin.check_schema({ + function_uri = "https://api.amazonaws.com", + authorization = { + iam = { + accesskey = "key1", + secretkey = "key2" + } + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +done + + + +=== TEST 2: missing fields in iam schema +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.aws-lambda") + local ok, err = plugin.check_schema({ + function_uri = "https://api.amazonaws.com", + authorization = { + iam = { + secretkey = "key2" + } + } + }) + if not ok then + ngx.say(err) + else + ngx.say("done") + end + } + } +--- response_body +property "authorization" validation failed: property "iam" validation failed: property "accesskey" is required + + + +=== TEST 3: create route with aws plugin enabled +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "aws-lambda": { + "function_uri": "http://localhost:8765/httptrigger", + "authorization": { + "apikey" : "testkey" + } + } + }, + "uri": "/aws" + }]], + [[{ + "node": { + "value": { + "plugins": { + "aws-lambda": { + "keepalive": true, + "timeout": 3000, + "ssl_verify": true, + "keepalive_timeout": 60000, + "keepalive_pool": 5, + "function_uri": "http://localhost:8765/httptrigger", + "authorization": { + "apikey": "testkey" + } + } + }, + "uri": "/aws" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: test plugin endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local core = require("apisix.core") + + local code, _, body, headers = t("/aws", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + -- headers proxied 2 times -- one by plugin, another by this test case + core.response.set_header(headers) + ngx.print(body) + } + } +--- response_body +aws lambda invoked +--- response_headers +Content-Length: 19 + + + +=== TEST 5: check authz header - apikey +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- passing an apikey + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "aws-lambda": { + "function_uri": "http://localhost:8765/generic", + "authorization": { + "apikey": "test_key" + } + } + }, + "uri": "/aws" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + local code, _, body = t("/aws", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.print(body) + } + } +--- inside_lua_block +local headers = ngx.req.get_headers() or {} +ngx.say("Authz-Header - " .. headers["x-api-key"] or "") + +--- response_body +passed +Authz-Header - test_key + + + +=== TEST 6: check authz header - IAM v4 signing +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + + -- passing the iam access and secret keys + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "aws-lambda": { + "function_uri": "http://localhost:8765/generic", + "authorization": { + "iam": { + "accesskey": "KEY1", + "secretkey": "KeySecret" + } + } + } + }, + "uri": "/aws" + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + ngx.say(body) + + local code, _, body, headers = t("/aws", "GET") + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.print(body) + } + } +--- inside_lua_block +local headers = ngx.req.get_headers() or {} +ngx.say("Authz-Header - " .. headers["Authorization"] or "") +ngx.say("AMZ-Date - " .. headers["X-Amz-Date"] or "") +ngx.print("invoked") + +--- response_body eval +qr/passed +Authz-Header - AWS4-HMAC-SHA256 [ -~]* +AMZ-Date - [\d]+T[\d]+Z +invoked/ From 62379b2ded64821000556abee9147c69b6a7565d Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Wed, 1 Dec 2021 17:36:19 +0800 Subject: [PATCH 147/260] docs(how-to-build): remove the APISIX RPM package installation (#5659) --- docs/en/latest/how-to-build.md | 8 -------- docs/zh/latest/how-to-build.md | 8 -------- 2 files changed, 16 deletions(-) diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 2cabe4c22aa8..96d9f1212938 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -53,14 +53,6 @@ If the official OpenResty repository is not installed yet, the following command sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm ``` -### Installation via RPM Package(CentOS 7) - -This installation method is suitable for CentOS 7, please run the following command to install Apache APISIX. - -```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.11.0-0.el7.x86_64.rpm -``` - ### Installation via Docker Please refer to: [Installing Apache APISIX with Docker](https://hub.docker.com/r/apache/apisix). diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index 520f80d3d8e1..f997ab17bf3b 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -53,14 +53,6 @@ sudo yum install apisix sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm ``` -### 通过 RPM 包安装(CentOS 7) - -这种安装方式适用于 CentOS 7 操作系统,请运行以下命令安装 Apache APISIX。 - -```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/7/x86_64/apisix-2.11.0-0.el7.x86_64.rpm -``` - ### 通过 Docker 安装 详情请参考:[使用 Docker 安装 Apache APISIX](https://hub.docker.com/r/apache/apisix)。 From 13c119f55957f9abde71847f4874ccc25b3ae20a Mon Sep 17 00:00:00 2001 From: Bisakh Date: Thu, 2 Dec 2021 06:21:28 +0530 Subject: [PATCH 148/260] docs: add aws lambda plugin to readme (#5655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index abce5e2a5733..1591e542230e 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,9 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - **Serverless** - [Lua functions](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX. - - [Azure functions](docs/en/latest/plugins/azure-functions.md): seamless integration with Azure Serverless Function as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud. - - [Apache OpenWhisk](docs/en/latest/plugins/openwhisk.md): seamless integration with Apache OpenWhisk as a dynamic upstream to proxy all requests for a particular URI to your own OpenWhisk cluster. + - [AWS Lambda](docs/en/latest/plugins/aws-lambda.md): Integration with AWS Lambda function as a dynamic upstream to proxy all requests for a particular URI to the AWS API gateway endpoint. Supports authorization via api key and AWS IAM access secret. + - [Azure Functions](docs/en/latest/plugins/azure-functions.md): Seamless integration with Azure Serverless Function as a dynamic upstream to proxy all requests for a particular URI to the Microsoft Azure cloud. + - [Apache OpenWhisk](docs/en/latest/plugins/openwhisk.md): Seamless integration with Apache OpenWhisk as a dynamic upstream to proxy all requests for a particular URI to your own OpenWhisk cluster. ## Get Started From 9a3931f3b8c011e7f7fed7921a2b7ba98cb66fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 3 Dec 2021 08:54:07 +0800 Subject: [PATCH 149/260] docs: update serverless example format error (#5665) --- docs/en/latest/plugins/serverless.md | 4 ++-- docs/zh/latest/plugins/serverless.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/latest/plugins/serverless.md b/docs/en/latest/plugins/serverless.md index 70871ab13f7c..3530cff7d3a5 100644 --- a/docs/en/latest/plugins/serverless.md +++ b/docs/en/latest/plugins/serverless.md @@ -76,7 +76,7 @@ Since `v2.6`, we pass the `conf` and `ctx` as the first two arguments to the ser Here's an example, enable the serverless plugin on the specified route: ```shell -curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/index.html", "plugins": { @@ -87,7 +87,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "serverless-post-function": { "phase": "rewrite", "functions" : ["return function(conf, ctx) ngx.log(ngx.ERR, \"match uri \", ctx.curr_req_matched and ctx.curr_req_matched._path); end"] - }, + } }, "upstream": { "type": "roundrobin", diff --git a/docs/zh/latest/plugins/serverless.md b/docs/zh/latest/plugins/serverless.md index bb160be377e1..9ad6bbadf98f 100644 --- a/docs/zh/latest/plugins/serverless.md +++ b/docs/zh/latest/plugins/serverless.md @@ -67,7 +67,7 @@ ngx.say(count) 下面是一个示例,在指定的 route 上开启了 serverless 插件: ```shell -curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "uri": "/index.html", "plugins": { @@ -78,7 +78,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f03433 "serverless-post-function": { "phase": "rewrite", "functions" : ["return function(conf, ctx) ngx.log(ngx.ERR, \"match uri \", ctx.curr_req_matched and ctx.curr_req_matched._path); end"] - }, + } }, "upstream": { "type": "roundrobin", From 9b7490630cab985975b345dfa9ca922eed487667 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:54:18 +0800 Subject: [PATCH 150/260] test(core.log): fix unit test case (#5660) --- t/core/log.t | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/t/core/log.t b/t/core/log.t index b1d61efab23f..6e847d02bcbc 100644 --- a/t/core/log.t +++ b/t/core/log.t @@ -155,20 +155,46 @@ debug log --- config location /t { content_by_lua_block { - local log = require("apisix.core").log.new("test: ") - log.error("error log") - log.warn("warn log") - log.notice("notice log") - log.info("info log") + local log_prefix = require("apisix.core").log.new("prefix: ") + log_prefix.error("error log") + log_prefix.warn("warn log") + log_prefix.notice("notice log") + log_prefix.info("info log") ngx.say("done") } } --- log_level: error --- request GET /t ---- error_log -error log +--- error_log eval +qr/[error].+prefix: error log/ +--- no_error_log +[qr/[warn].+warn log/, qr/[notice].+notice log/, qr/[info].+info log/] + + + +=== TEST 7: print both prefixed error logs and normal logs +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local log_prefix = core.log.new("prefix: ") + core.log.error("raw error log") + core.log.warn("raw warn log") + core.log.notice("raw notice log") + core.log.info("raw info log") + + log_prefix.error("error log") + log_prefix.warn("warn log") + log_prefix.notice("notice log") + log_prefix.info("info log") + ngx.say("done") + } + } +--- log_level: error +--- request +GET /t +--- error_log eval +[qr/[error].+raw error log/, qr/[error].+prefix: error log/] --- no_error_log -test: warn log -test: notice log -test: info log +[qr/[warn].+warn log/, qr/[notice].+notice log/, qr/[info].+info log/] From 4bc635f5d6e777d8ca9d2cd5fdef0127883e13d3 Mon Sep 17 00:00:00 2001 From: "huang.xinghui" Date: Fri, 3 Dec 2021 09:35:12 +0800 Subject: [PATCH 151/260] docs(prometheus): Fix docs broken link (#5674) --- docs/zh/latest/plugins/prometheus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/latest/plugins/prometheus.md b/docs/zh/latest/plugins/prometheus.md index 3cbbf565857a..2f5737b674fe 100644 --- a/docs/zh/latest/plugins/prometheus.md +++ b/docs/zh/latest/plugins/prometheus.md @@ -137,7 +137,7 @@ plugin_attr: 插件导出的指标可以在 Grafana 进行图形化绘制显示。 -下载 [Grafana dashboard 元数据](../../../assets/other/json/apisix-grafana-dashboard.json) 并导入到 Grafana 中。 +下载 [Grafana dashboard 元数据](https://github.com/apache/apisix/blob/master/docs/assets/other/json/apisix-grafana-dashboard.json) 并导入到 Grafana 中。 你可以到 [Grafana 官方](https://grafana.com/grafana/dashboards/11719) 下载 `Grafana` 元数据. From b557759834f8ed4afe1bb79c6889de406f1a80a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 3 Dec 2021 09:39:41 +0800 Subject: [PATCH 152/260] docs(log-rotate): fix typo (#5667) --- apisix/plugins/log-rotate.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/log-rotate.lua b/apisix/plugins/log-rotate.lua index b861f7f9c935..273dc01d3c5b 100644 --- a/apisix/plugins/log-rotate.lua +++ b/apisix/plugins/log-rotate.lua @@ -209,11 +209,11 @@ local function rotate() return end - core.log.warn("send USER1 signal to master process [", + core.log.warn("send USR1 signal to master process [", process.get_master_pid(), "] for reopening log file") local ok, err = signal.kill(process.get_master_pid(), signal.signum("USR1")) if not ok then - core.log.error("failed to send USER1 signal for reopening log file: ", err) + core.log.error("failed to send USR1 signal for reopening log file: ", err) end -- clean the oldest file From b8daf65b3c0690138feccb2ce399fc58349ac7ee Mon Sep 17 00:00:00 2001 From: Bisakh Date: Fri, 3 Dec 2021 07:12:05 +0530 Subject: [PATCH 153/260] docs: documenting url path forwarding for faas plugins (#5664) --- docs/en/latest/plugins/aws-lambda.md | 42 +++++++++++++++++++++++ docs/en/latest/plugins/azure-functions.md | 39 +++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/docs/en/latest/plugins/aws-lambda.md b/docs/en/latest/plugins/aws-lambda.md index 5afb805a1071..87fccedeafde 100644 --- a/docs/en/latest/plugins/aws-lambda.md +++ b/docs/en/latest/plugins/aws-lambda.md @@ -28,6 +28,7 @@ title: aws-lambda - [Attributes](#attributes) - [IAM Authorization Schema](#iam-authorization-schema) - [How To Enable](#how-to-enable) + - [Plugin with Path Forwarding](#plugin-with-path-forwarding) - [Disable Plugin](#disable-plugin) ## Name @@ -136,6 +137,47 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 **Note**: This approach assumes you already have an iam user with the programmatic access enabled and required permissions (`AmazonAPIGatewayInvokeFullAccess`) to access the endpoint. +### Plugin with Path Forwarding + +AWS Lambda plugin supports url path forwarding while proxying request to the modified upstream (AWS Gateway URI endpoint). With that being said, any extension to the path of the base request APISIX gateway URI gets "appended" (path join) to the `function_uri` specified in the plugin configuration. + +**Note**: APISIX route uri must be ended with an asterisk (`*`) for this feature to work properly. APISIX routes are strictly matched and the extra asterisk at the suffix means any subpath appended to the original parent path will use the same route object configurations. + +Here is an example: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "aws-lambda": { + "function_uri": "https://x9w6z07gb9.execute-api.us-east-1.amazonaws.com", + "authorization": { + "apikey": "" + }, + "ssl_verify":false + } + }, + "uri": "/aws/*" +}' +``` + +Now any request with path `aws/default/test-apisix` will invoke the aws api gateway endpoint. Here the extra path (where the magic character `*` has been used) upto the query params have been forwarded. + +```shell +curl -i -XGET http://127.0.0.1:9080/aws/default/test-apisix\?name\=APISIX +HTTP/1.1 200 OK +Content-Type: application/json +Connection: keep-alive +Date: Wed, 01 Dec 2021 14:23:27 GMT +X-Amzn-Trace-Id: Root=1-61a7855f-0addc03e0cf54ddc683de505;Sampled=0 +x-amzn-RequestId: f5f4e197-9cdd-49f9-9b41-48f0d269885b +Content-Length: 16 +x-amz-apigw-id: JrHG8GC4IAMFaGA= +Server: APISIX/2.11.0 + +"Hello, APISIX!" +``` + ## Disable Plugin Remove the corresponding JSON configuration in the plugin configuration to disable the `aws-lambda` plugin and add the suitable upstream configuration. diff --git a/docs/en/latest/plugins/azure-functions.md b/docs/en/latest/plugins/azure-functions.md index 1f9bdd7ad9a0..3eb96d4a12b9 100644 --- a/docs/en/latest/plugins/azure-functions.md +++ b/docs/en/latest/plugins/azure-functions.md @@ -28,6 +28,7 @@ title: azure-functions - [Attributes](#attributes) - [Metadata](#metadata) - [How To Enable](#how-to-enable) + - [Plugin with Path Forwarding](#plugin-with-path-forwarding) - [Disable Plugin](#disable-plugin) ## Name @@ -120,6 +121,44 @@ server: APISIX/2.10.2 Hello, APISIX ``` +### Plugin with Path Forwarding + +Azure Faas plugin supports url path forwarding while proxying request to the modified upstream. With that being said, any extension to the path of the base request APISIX gateway URI gets "appended" (path join) to the `function_uri` specified in the plugin configuration. + +**Note**: APISIX route uri must be ended with an asterisk (`*`) for this feature to work properly. APISIX routes are strictly matched and the extra asterisk at the suffix means any subpath appended to the original parent path will use the same route object configurations. + +Here is an example: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "azure-functions": { + "function_uri": "http://app-bisakh.azurewebsites.net/api", + "authorization": { + "apikey": "" + } + } + }, + "uri": "/azure/*" +}' +``` + +Now any request with path `azure/HttpTrigger1` will invoke the azure function. Here the extra path (where the magic character `*` has been used) upto the query params have been forwarded. + +```shell +curl -i -XGET http://127.0.0.1:9080/azure/HttpTrigger1\?name\=APISIX +HTTP/1.1 200 OK +Content-Type: text/plain; charset=utf-8 +Transfer-Encoding: chunked +Connection: keep-alive +Date: Wed, 01 Dec 2021 14:19:53 GMT +Request-Context: appId=cid-v1:4d4b6221-07f1-4e1a-9ea0-b86a5d533a94 +Server: APISIX/2.11.0 + +Hello, APISIX +``` + ## Disable Plugin Remove the corresponding JSON configuration in the plugin configuration to disable the `azure-functions` plugin and add the suitable upstream configuration. From 11433d740c5e19b757c61f59842d6de64e27fba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 3 Dec 2021 09:58:44 +0800 Subject: [PATCH 154/260] feat(mqtt-proxy): support using route's upstream (#5666) --- apisix/stream/plugins/mqtt-proxy.lua | 6 +++- docs/en/latest/plugins/mqtt-proxy.md | 15 +++++--- docs/zh/latest/plugins/mqtt-proxy.md | 15 +++++--- t/stream-plugin/mqtt-proxy.t | 53 ++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/apisix/stream/plugins/mqtt-proxy.lua b/apisix/stream/plugins/mqtt-proxy.lua index f5df120df232..13318e4114e1 100644 --- a/apisix/stream/plugins/mqtt-proxy.lua +++ b/apisix/stream/plugins/mqtt-proxy.lua @@ -41,7 +41,7 @@ local schema = { }, } }, - required = {"protocol_name", "protocol_level", "upstream"}, + required = {"protocol_name", "protocol_level"}, } @@ -164,6 +164,10 @@ function _M.preread(conf, ctx) core.log.info("mqtt client id: ", res.client_id) + if not conf.upstream then + return + end + local host = conf.upstream.host if not host then host = conf.upstream.ip diff --git a/docs/en/latest/plugins/mqtt-proxy.md b/docs/en/latest/plugins/mqtt-proxy.md index 50bf44596591..93d0ef7909cc 100644 --- a/docs/en/latest/plugins/mqtt-proxy.md +++ b/docs/en/latest/plugins/mqtt-proxy.md @@ -41,6 +41,7 @@ And this plugin both support MQTT protocol [3.1.*](http://docs.oasis-open.org/mq | -------------- | ------- | ----------- | ------- | ----- | -------------------------------------------------------------------------------------- | | protocol_name | string | required | | | Name of protocol, should be `MQTT` in normal. | | protocol_level | integer | required | | | Level of protocol, it should be `4` for MQTT `3.1.*`. it should be `5` for MQTT `5.0`. | +| upstream | object | deprecated | | | Use separate upstream in the route instead. | | upstream.host | string | required | | | the IP or host of upstream, will forward current request to. | | upstream.ip | string | deprecated | | | Use "host" instead. IP address of upstream, will forward current request to.| | upstream.port | number | required | | | Port of upstream, will forward current request to. | @@ -73,12 +74,16 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 "plugins": { "mqtt-proxy": { "protocol_name": "MQTT", - "protocol_level": 4, - "upstream": { - "host": "127.0.0.1", - "port": 1980 - } + "protocol_level": 4 } + }, + "upstream": { + "type": "roundrobin", + "nodes": [{ + "host": "127.0.0.1", + "port": 1980, + "weight": 1 + }] } }' ``` diff --git a/docs/zh/latest/plugins/mqtt-proxy.md b/docs/zh/latest/plugins/mqtt-proxy.md index be70b93f190b..3a0aff7ad6c3 100644 --- a/docs/zh/latest/plugins/mqtt-proxy.md +++ b/docs/zh/latest/plugins/mqtt-proxy.md @@ -40,6 +40,7 @@ title: mqtt-proxy | -------------- | ------- | ------ | ------ | ------ | ------------------------------------------------------ | | protocol_name | string | 必须 | | | 协议名称,正常情况下应为“ MQTT” | | protocol_level | integer | 必须 | | | 协议级别,MQTT `3.1.*` 应为 `4` ,MQTT `5.0` 应是`5`。 | +| upstream | object | 废弃 | | | 推荐改用 route 上配置的上游信息 | | upstream.host | string | 必须 | | | 将当前请求转发到的上游的 IP 地址或域名 | | upstream.ip | string | 废弃 | | | 推荐使用“host”代替。将当前请求转发到的上游的 IP 地址 | | upstream.port | number | 必须 | | | 将当前请求转发到的上游的端口 | @@ -71,12 +72,16 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 "plugins": { "mqtt-proxy": { "protocol_name": "MQTT", - "protocol_level": 4, - "upstream": { - "host": "127.0.0.1", - "port": 1980 - } + "protocol_level": 4 } + }, + "upstream": { + "type": "roundrobin", + "nodes": [{ + "host": "127.0.0.1", + "port": 1980, + "weight": 1 + }] } }' ``` diff --git a/t/stream-plugin/mqtt-proxy.t b/t/stream-plugin/mqtt-proxy.t index 0aa879920603..4f3796ba9379 100644 --- a/t/stream-plugin/mqtt-proxy.t +++ b/t/stream-plugin/mqtt-proxy.t @@ -265,3 +265,56 @@ passed --- error_log failed to parse domain: loc, error: --- timeout: 10 + + + +=== TEST 11: set route with upstream +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/stream_routes/1', + ngx.HTTP_PUT, + [[{ + "remote_addr": "127.0.0.1", + "server_port": 1985, + "plugins": { + "mqtt-proxy": { + "protocol_name": "MQTT", + "protocol_level": 4 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": [{ + "host": "127.0.0.1", + "port": 1995, + "weight": 1 + }] + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 12: hit route +--- stream_enable +--- stream_request eval +"\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" +--- stream_response +hello world +--- no_error_log +[error] From f21a78c0f90e81618fbad4b2a863a78ba1aa4ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 3 Dec 2021 17:44:34 +0800 Subject: [PATCH 155/260] chore(cli): update error command prompt (#5681) --- apisix/cli/env.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apisix/cli/env.lua b/apisix/cli/env.lua index 838ef63f8db8..1f19e6a5b21a 100644 --- a/apisix/cli/env.lua +++ b/apisix/cli/env.lua @@ -72,7 +72,7 @@ return function (apisix_home, pkg_cpath_org, pkg_path_org) if ok and json then stderr:write("please remove the cjson library in Lua, it may " .. "conflict with the cjson library in openresty. " - .. "\n luarocks remove cjson\n") + .. "\n luarocks remove lua-cjson\n") exit(1) end end From 6e09d56e9c93666b52fccdeb6e1d3fb1178b678c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 5 Dec 2021 19:29:00 +0800 Subject: [PATCH 156/260] chore(ext-plugin): we already handled extraInfo (#5685) --- apisix/plugins/ext-plugin/init.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index 1e3ff0c3da13..772bd3832ed3 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -514,7 +514,6 @@ local rpc_handlers = { http_req_call_req.AddArgs(builder, args_vec) http_req_call_req.AddHeaders(builder, hdrs_vec) http_req_call_req.AddMethod(builder, encode_a6_method(method)) - -- TODO: handle extraInfo local req = http_req_call_req.End(builder) builder:Finish(req) From 319f6208ba699ac3f353318dd55664376d48314d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 5 Dec 2021 20:33:16 +0800 Subject: [PATCH 157/260] test: we don't need to check the response of http://apisix.apache.org (#5699) --- t/misc/patch.t | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/t/misc/patch.t b/t/misc/patch.t index b479f072a99c..a735126df3db 100644 --- a/t/misc/patch.t +++ b/t/misc/patch.t @@ -144,13 +144,13 @@ apisix: ngx.log(ngx.ERR, err) return end - ngx.say(res.status) + ngx.say("ok") } } --- request GET /t --- response_body -200 +ok @@ -176,11 +176,11 @@ apisix: ngx.log(ngx.ERR, err) return ngx.exit(-1) end - sock:send(res.status) + sock:send("ok") } --- stream_request eval m ---- stream_response: 200 +--- stream_response: ok --- no_error_log [error] From 4df272c548b2b1f58331cd495424b078dea12bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 6 Dec 2021 10:02:13 +0800 Subject: [PATCH 158/260] fix(batch-processor): we didn't free stale object actually (#5700) --- apisix/plugins/datadog.lua | 3 ++- apisix/plugins/google-cloud-logging.lua | 4 ++-- apisix/plugins/http-logger.lua | 3 ++- apisix/plugins/kafka-logger.lua | 3 +-- apisix/plugins/skywalking-logger.lua | 4 ++-- apisix/plugins/sls-logger.lua | 3 ++- apisix/plugins/syslog.lua | 4 ++-- apisix/plugins/tcp-logger.lua | 4 ++-- apisix/plugins/udp-logger.lua | 4 ++-- 9 files changed, 17 insertions(+), 15 deletions(-) diff --git a/apisix/plugins/datadog.lua b/apisix/plugins/datadog.lua index 6a55886f3a9c..b613bac96bc1 100644 --- a/apisix/plugins/datadog.lua +++ b/apisix/plugins/datadog.lua @@ -26,6 +26,7 @@ local format = string.format local concat = table.concat local buffers = {} local ipairs = ipairs +local pairs = pairs local tostring = tostring local stale_timer_running = false local timer_at = ngx.timer.at @@ -120,7 +121,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) diff --git a/apisix/plugins/google-cloud-logging.lua b/apisix/plugins/google-cloud-logging.lua index 961762d2e27f..a233d3f5ee8d 100644 --- a/apisix/plugins/google-cloud-logging.lua +++ b/apisix/plugins/google-cloud-logging.lua @@ -18,7 +18,7 @@ local core = require("apisix.core") local ngx = ngx local tostring = tostring -local ipairs = ipairs +local pairs = pairs local ngx_timer_at = ngx.timer.at local http = require("resty.http") local log_util = require("apisix.utils.log-util") @@ -128,7 +128,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, route id:", tostring(key)) buffers[key] = nil diff --git a/apisix/plugins/http-logger.lua b/apisix/plugins/http-logger.lua index b52c1adcbae3..a4eadb3f0857 100644 --- a/apisix/plugins/http-logger.lua +++ b/apisix/plugins/http-logger.lua @@ -25,6 +25,7 @@ local plugin = require("apisix.plugin") local ngx = ngx local tostring = tostring local ipairs = ipairs +local pairs = pairs local timer_at = ngx.timer.at local plugin_name = "http-logger" @@ -166,7 +167,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua index def042feb2be..01439225575b 100644 --- a/apisix/plugins/kafka-logger.lua +++ b/apisix/plugins/kafka-logger.lua @@ -23,7 +23,6 @@ local plugin = require("apisix.plugin") local math = math local pairs = pairs local type = type -local ipairs = ipairs local plugin_name = "kafka-logger" local stale_timer_running = false local timer_at = ngx.timer.at @@ -164,7 +163,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) diff --git a/apisix/plugins/skywalking-logger.lua b/apisix/plugins/skywalking-logger.lua index 58f25b84b0f7..bd5f9c1169eb 100644 --- a/apisix/plugins/skywalking-logger.lua +++ b/apisix/plugins/skywalking-logger.lua @@ -28,7 +28,7 @@ local ngx_re = require("ngx.re") local ngx = ngx local tostring = tostring local tonumber = tonumber -local ipairs = ipairs +local pairs = pairs local timer_at = ngx.timer.at local plugin_name = "skywalking-logger" @@ -130,7 +130,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) diff --git a/apisix/plugins/sls-logger.lua b/apisix/plugins/sls-logger.lua index daeaebb7dff0..797b85fd8894 100644 --- a/apisix/plugins/sls-logger.lua +++ b/apisix/plugins/sls-logger.lua @@ -26,6 +26,7 @@ local tcp = ngx.socket.tcp local buffers = {} local tostring = tostring local ipairs = ipairs +local pairs = pairs local table = table local schema = { type = "object", @@ -116,7 +117,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, route id:", tostring(key)) buffers[key] = nil diff --git a/apisix/plugins/syslog.lua b/apisix/plugins/syslog.lua index a3ec82292fb3..3eed59ff433f 100644 --- a/apisix/plugins/syslog.lua +++ b/apisix/plugins/syslog.lua @@ -22,7 +22,7 @@ local logger_socket = require("resty.logger.socket") local plugin_name = "syslog" local ngx = ngx local buffers = {} -local ipairs = ipairs +local pairs = pairs local stale_timer_running = false; local timer_at = ngx.timer.at @@ -121,7 +121,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) diff --git a/apisix/plugins/tcp-logger.lua b/apisix/plugins/tcp-logger.lua index dc4cb2121bae..8d678b33ca1b 100644 --- a/apisix/plugins/tcp-logger.lua +++ b/apisix/plugins/tcp-logger.lua @@ -22,7 +22,7 @@ local tostring = tostring local buffers = {} local ngx = ngx local tcp = ngx.socket.tcp -local ipairs = ipairs +local pairs = pairs local stale_timer_running = false local timer_at = ngx.timer.at @@ -106,7 +106,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) diff --git a/apisix/plugins/udp-logger.lua b/apisix/plugins/udp-logger.lua index 06f903e6d607..97e6552cfd65 100644 --- a/apisix/plugins/udp-logger.lua +++ b/apisix/plugins/udp-logger.lua @@ -22,7 +22,7 @@ local tostring = tostring local buffers = {} local ngx = ngx local udp = ngx.socket.udp -local ipairs = ipairs +local pairs = pairs local stale_timer_running = false; local timer_at = ngx.timer.at @@ -90,7 +90,7 @@ local function remove_stale_objects(premature) return end - for key, batch in ipairs(buffers) do + for key, batch in pairs(buffers) do if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then core.log.warn("removing batch processor stale object, conf: ", core.json.delay_encode(key)) From 2d32547ee807a245273328f898774f2535b4962d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 6 Dec 2021 10:04:59 +0800 Subject: [PATCH 159/260] chore: upgrade tinyyaml to 0.4.1 (#5678) --- rockspec/apisix-master-0.rockspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index f3e37f08c0bc..56b1f59ffe50 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -49,7 +49,7 @@ dependencies = { "lua-protobuf = 0.3.3", "lua-resty-openidc = 1.7.2-1", "luafilesystem = 1.7.0-2", - "api7-lua-tinyyaml = 0.3.0", + "api7-lua-tinyyaml = 0.4.1", "nginx-lua-prometheus = 0.20210206", "jsonschema = 0.9.5", "lua-resty-ipmatcher = 0.6.1", From 31f99bc6511395d816dbb99442fb194e4fce838b Mon Sep 17 00:00:00 2001 From: Soham Banerjee <63705023+soham4abc@users.noreply.github.com> Date: Mon, 6 Dec 2021 07:42:14 +0530 Subject: [PATCH 160/260] docs: Software architecture diagram added (#5698) Signed-off-by: Soham Banerjee --- .../images/flow-software-architecture.png | Bin 0 -> 334625 bytes docs/en/latest/architecture-design/apisix.md | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 docs/assets/images/flow-software-architecture.png diff --git a/docs/assets/images/flow-software-architecture.png b/docs/assets/images/flow-software-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..633d04c55babe2d325f2186013fbab0562f46e74 GIT binary patch literal 334625 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

Oa0gj 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%0Aw+uWML|Xk^Z*Zf|aVrKK&3 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⪼_fDWvXh!jT~}OP(A};OkK=1eKh^ z&u@2EG^*StB&ckqZ5{JQrqnj17tmz1=NCR!cXs*9nGJ5NNkC*%9|#9#gOKRoWzCUV zAEn0o=1Dfg)9Fvwur^25{GKjlUjc7&llJgYd;f&jf;YY58A_)MV}5n9^tx!}pD12G zs?TlxrmxCalpczu&B0_{kYDDaXA3x3FOTb53j0eDCEnajzJhlii zJPs0ZorR!<<0oc^!TQrxMonCfnrSOWa2;E79BgNf<6Ac+T62b2*i~I28`1Kzy$usK zxSy8M)Tyv82mF0o^^A51z#T>#8BPk+|s>a)^7H0pSAgEqXx3AqgUZ z#5+>NPUFP(csKAn5PbGD(N*HoLeJPYE%5s7jEX%#EBN>~2v~_9b#5q_Suk^DjPYUR z^Yjb4sw`=H-u+p`w$UC@(QG?~WcLiUgn`}9jbc!ms9;3hZHo;~RM&@d4%+kw5Xx3o zG^N1aZfdnm*Z6X%qsyzSdfvl@TAPmQ#DTGp#{GSBYK@v9t8S#ad6YL-S?tp@GaL5@ zWxXB#FrpPkCoZR}0*CX}FiT5IDMBXNYE&c#PBU78(?u!~lEG)Li&tmEY%AF1wHKV# zo_F>$qvz-LVmM7e@w(~ir$(Qlp%NSG^wgB@)BTmHnHh$M9;Qh!Y=hAtY6`cTy`Y($ z>=IC$aqtXz@s%RC#Q5dY{4ns--9+fHo4t!LI2Um(9zK=3%}veH&ggrk6xd}e`&PNS+g zIA&#!$N8YBO%RxiiEVD>!pm7GZg4k#Q?t?HQY_?UVD>s{=g{$*2DFy?9*dOk8}$}z z^rDz6QHGql2YnU#OTs3Zm8Z=D8_L zA?3cTj#41Udw*q>X0!Q z)6&wq9gpz-DvIm2*>ouo=MI(?N2Rh(gm?GtAQ0^Z;8vE~bY8A>8tc-#aq6~{EKWUI zZ#;;=vJ4r6RJ7e7R_;bze%NjFT#(WOV-6iB&&MZ%URR0eT=tjoK97l{H)}xJ&G;C&Sf^y;;o-qR z>Ovh4$8!naH5C2J9qY0}nWkMXx52FdYpcP$hcf#5K#6XzcP+yQ=l{_2^fyVo+JVR} zu#=fbPWwry6H-BzILen5DzwQ!#x#4BqM1%edsbWHKt%T%iWm`!j5;dlb8U@UA=)VK z73y`CH4$I3xFoLo^kH#!X7JKf@Hid_l6MY>SVJ&!xyiS0J1|7hNd|WoJ=dEG*~|9vTWL?@i6KX<%J7>#tgIm zcHgxVil&NZ0m6GSB#*a1aD$HLPR&4OulQk zNXV)#eD9F9^mQP7InjFJpIPQrMl$@pPOK}2CeAW2NV~OE5Z5R6wV>ckIocGoE|K{J zM2tE0exqjz{Z1@`^lG7!R`X`1D81mUzEI@#_BMVn8NL=QviXLPN|Pg3Q+l@%E3@Lx zH%SCNg&NXTPW9!@2X8Fs6C=e(c(r4{yE#H~o;ZAUn-q_6Ujf?6mrz)dlmD98q|RNHabB z4byH&F3O3pDY%bUn=Wu?(o!+~Dku&~bB;k?J7 z0$Xmo=^DP+jG;7|i{l6Ae z{B7BP_c@YFrE?*+`R$WCv&8DL4++~eAnrMc%4ibnMYZgfILH_hvk{HzEj<)ZJRDXy zQpH<<)6#(f8G>vVHdM%E(|79Tvw4QYc{}JJ*Cb(LlEh|Wm$WR=1_tg}!D7i{c9Vnv z*B3kJY~nx+DUNW8jh+>rG=6pWI>5D=l2(cC}xD{G~uZM`iK zpTt6AZxEut2g>6I2avO5LPdX$-6I#TC4vfeEOl;@3KzHk6n#skvyvYvHbYFK|HQ$I zIuhPTC90>8!EFO9<)GyP+<3Ggoip>Aii*nU>>FaeU{X0w0NZOKFp1-Hyo5Y&_y@jcN_PH z<49{}pSl4l@p4X-mzUSmw>q(4LnO4Y^zvU*F2+H=&jDs5GujvR%R?5=6UpeqFCPa{ z^?ek0YA#^ITZ`b@{RlP@J=+Hv8`5%~$ZTAKAJz6NSPcHqw)}sxoHk)0{%xFoUp%iU zd_)0SD~ECo5SvIHicf)DCSg!Wu&{_a(u7I0Hpdb$x2S0bUbS7wqE~+|d4+v@ckwVj z&g&|0Dz-o2oR3x3?2cT!G;Xi#C97I{WRubs^hGOnM3UKhY*zbT?I0G6sO0tFn846T zQCpvxd(|xgLqvijv-|c|VeD6d?hdh~oL#fm0i1&?nGbaz6vz*pkNHN0O_|AjHi!N1 zuFuzr%Ge+nqA*7e;uWhUN^6%(?ux~qMv8>#?SfUU9?<~Zz^Je=#eP0MA;A3$Mr55+g`cK0>4%}1eKGC7zreYzhdy}dni-w_&TwRMT!FC!<9;U0DzYV}P0Ga;jiuCMp~C6F9~LN`AtQZq)wAN2A)mpd@Z zzs!_qygR#}NND*=mgWiufHkCsqk1B7P2!y_COf|**+iq?jr=2izBk4?N@AMXcRCI! z*YmMWY*$G|n?iYP4~Xb)gjwqq%RI*-|6=U_eggk<2EdOhz_*rk!)trD9&ztN2tY_b zOc62Nl6Q2oM{%p&oHQB6EYn4N!HF9t9Rxw$GV#|(9R3EPZ*L)1--o2szwR=;N)z{v zC8$sHG{gc}o41P(ZAvEgR78u}XNQv>XO~D^6tt8i2z~YCW}zM~wFaGGFcLq@OnDIv zrMe3@!{W_=2{$8w^r=I;eppF5=SxEIjMi`*uI;Y^_Kxfc+4syG%WgZV(qrfQMP-6# zY$21j1YZ-+t5!YEH^uX2sc9W*dNE@(U72yA!bV)=u{3Hd+W}mOwA15QLx=AKQaq`E zPvLSY)c^+twMZCVX25y%DP7(8yxh#zJM5rGMa5P`@IBWVIA092aqZ;gfxO*6U&K(# zYC$(&*N=LQbO|$^t*YVxIz0sWdKly`P|9}Z-l>xkTXwMg?sbJZ4JvvV8ZpX>MejeW zJqLKxC}Q&{X7%!xS1GyQ_^s58q6kQ0ANS;$D)4*Q6yzqsfE6DKpgPsmeXsc!=Aw!drl57WQYT5lFv~9!C&{1A; z`$$AH8a7>-6~w`a>WOi+!SISJ`!rdS|gprBM-GkeTA&!e3F*_-S4vXvA# zu;rAZ=H)&rlpV9uiDb_pwW~0~C$Cd*ktC$%;e%whQ4bHgl|N0g{jI>7+Tg)_qEmg2 zrNa-B<4EXcWuvIgQXPaglPpNEn%b>stVv8n_?_=~*bL?A)#h9C-h6purwwF_UAN$3 zD5Zg;yv{!jwQTP^Yck`&(j$BhHEwluTy_1L8qdh^Zkoq2h8j8D^l{>*IR51t zoX_l1CcNoJYA7iZ>fYOWN-rRZ>oVbcP}6i6W+A2JL^@Ue31rO|(rq4-WdTia5SIZx zgE!kYP$ArYQDoH7kz&as|6H24H9{OS30fv?j|^`fMd8UK1QO?v;M5QysT-h!Kn3x& zS51pS?}yrO_QWYn>dHk|>rH2lKCuG!lyEyUWMYp+_aPf597x{}3=Cf|p~zkI$B z%RqdGnbku%lO>y9RTMMCz{$l`?zks!qA8b&IS5F5m61eN-ON4+wx*G*P_v zEzSC;AK7RMc||TYI=14}mAdVp#k&P#c)o_9(e(hc()c%c9C|5b!~#EarII!C8SD8e zocAP2{IwhRa+6Pom`3aDBx{%NE_OCr9*#ScSS%zpnYB!nM*x|sk)^XeBSPgJU44-s2y^eR| z&W;4NoOzeScsf2ntPc1PuFX;a4U#%GfYv8Ll7B0W&xGoE~E=KstY}~O+7&Eyi zzx=~xFR*#l-Am=54ak>{gho&3jo`;2v=JK26Q;X&4cdydw z#GX{t%9?6#&gpW`)O3DV#r~7y{LTS(fB1vlZKoSV;SNSPT9EHrX5Ub{xul^9!ol*R z^7qYz>e4S459AsH@%#8hg9Z~&(cIXKCmsdqWsu{k6H*(BT}JDVypZAiXN^CIs&m<< zS9@i1LPb{X-6s-<$r<2ai%C<`Phk4DK=N>Ndy*mAIyxL|$gf<1$@#k2F>gspiR(d0ZThK+N%Ca}(8FL9`9stvWrU>Q1#!PvyBYHra{;W8weOkU zdu}{w!IF}aqRrOu@MwgocIZJtv4&3u2oM2%5~$`LG8$hrI_HR1Z_=Ink2#U1IuG+R zr=57u@WFwcyh@6S?62eRr^gvsd3Vn?de4?lywL#gefukU#hHa8e-O0oG594$R;?QtQECL=HE+n} z_0Dp|G#~>WFn8@m_k$AJ&uxay5pa1?-|}ub9_Nx-C6#X^x89MHPB@o}8%(JN2Fk}M zD=8>!{9GP&I`Wu!d-xfEjA%4|gMzx7&0?4`YA}_xOU_ulFL$E`b0_w>^Ubs_0NL_hykW3ut6C4Z*S! z32m0pnyl)5B&9yd$%Iq@}m_qQ?B`c*(t&=J55_}nCi@P zO^l0+i>wU05iC@!KXa>SpX_2+Y8pPfiRDyDMg;E{76R*#Jf9CtE@f6@vrWzv2h+qt-{9;@*OL(~vF?I00plWRk#!RV&F>^8{Sup3^&yXW7Y28t*UWV9Dgoj^6OeUD9EPgp+ zbdq1u_;S6^!;hJhnSFe4)aTiOOg53tM*A#h5|^rKrv1yia-+fc$#@>E*FiQTd9QXH zk342b8r&Ywddn?lXz318ZV;)CmKxCjj+}3&;gb?R!y;@Vsr?~CNwTAP*c{x=yJ1WmW=A-`bKLgUJlbw2LzQ9i$rDE_k* zb{r4E%=O2R@|B-t|E^H~!3F_q`v7*Ww%&clu1ZJC3#Q(=1W@|FHaiqh$&d@_WH?0d zvhJ-qT+-f$w7T&vKP{u&Hv2r$-JU0Cr*Fst&kkHG=y?IuiiLMS&2pRda{K;yA4Vo3 zHp*apzZ&M0ul1C6(c?wQ;CND;FsfY!v>ftsi3e(X*0O?36EVy#Hy?^2u{rXfM2hS` zuKu-nnbzu8-r85`jQt{O`dT>?)5_$&qhF&5M`tI;(;EQZWc2Rai6+LD=n8g8`}2;R z>9)o)+-Kk|k6Uyb(<(YnBN7`Y$$UP%ypL;e9|?AU7OOFl@|sIX!0_Jn%LrO4>b%^~ zr2Z&W-6gS`0s?i)D=BG_LKC1ywvqh9;g_h|ghf{+Uvjk^-~9OSkBut7VPPF4q$y7& z0272W6R8hXm{Em@G1uKIK{~H+@w_dK-y*!fuN&FhVO_2_t$Nlq)yD{m*`_P#y}`W$ zkEbo^9Fv8X;j4ew=;DHp2({X9u)R^km9X0GQfzxH+Irn?qnl4A?UOxmWEzTD)WfaD z>3K_EMWa>$4h%&n6@T9XoQN$3;&)Huy034kdNHjOavV?^C6#V+9Hp?0L;Tm{J}o*l z^yVVB8uW&*MTz3JtF+!L?S#il=aC`DMBirNJ>?@Il&F?xW`C3XobiixcUhZgIgcx2 zhp@NPvM-E&m_Td26-Hw`GQb1+&}lSSGid(T3ah_KvHy4FWc8ddo$k_{hAHlo6z30M z7>$xrQ2J3pzu$i1VPq35!B*@1I_LPR)XgLA48p&;h;ZY!u@d=EG=Vx&OV6he}UN`SO$ixW=2 z9I^{ykTos(wMAf|VPs?kICAX>nD_29_EKA<)b0RnGv4mao ze!|SNp%*nRUreujNvU@8yg$Xoerh3zl1TgA2kf?+mK&bOm&2uZp4dByJ9l}72;$&~ z;NKlk9c1H+C8;#x4aGm$Ul;Q#JyP@r2i+BUkg)OuQtDm;x3p=^em6I2o+-~b^$ws}6DtjvvqT&WI zS|&9YE}t#1XXMcoxdxowP-!b#%aoOrj26CngTtfef^`z=8uDG9G+jtP`7P7XHD5+! z^SEDX=I8RAXn1)w1I0pW$Y#~JWQ9EGocAUqD?OnYNSAYPvKrbAj%EX_tX>P*;xUfe zBd=4wD@2&?l-sTeN{#-^!CommV`gRs_sS9pgYID8+xcGc5djr3=qocD+aOR}B3IGz za)KYL(4)yWQb?LRJL2OUXEhp##mBKcS-KeIqqCSPg?-MdGVG7uHEy@?XtilPVyy>w z7^C?rv+}D%PD>EiaUBsP#?x&tU;NQRt;r*%TB~>SZ2j!@U3luA>m`BjD=|NO#l z1~a>2t5T+&dO=)Nh{)(>{^dEf{L@I4%l4x4n2Iy!dBV%{%(=?TP0U2;WR{WyAv;7e z=kPP;0eeg61jT3}rN_Mo7hj`DqjKZ6CC#ITxq*Q}jnuB_D?dF-dTKBcR8zHbeSgXC z^aPOAz)B5j1J~@an=#<;W98AVsAA>zHsi*6xJ6?I!(bFHOHK)zu513|QlE9l7wa9q z6m-ZdBfGViz5u}HldKs9rz1Ld1uqQ07eKPdCtEjAG>DJuEckrWP**p6e!OdUaD4^C zz68KE0eDItUBk)Lxq9{W>FbBb9X?QXv9^MOf^7M~@`+&5ljun45+&}@ax>S)%r_;~ ziks*zD}oBkkbjm|+bPO-u2!|8`>DRDPUT3#>d1~U?P|K-%an%hm`^1xQTZj2NrmPZ zOR_W@Nuq<7a~qq-jR^xoa|T1J6$nMnI*W5$NFv6;0yO`N%cd)AzF$ZvT)?e2KKI zOS-2#c+LzdqomF<_wkACI9^zyXRts)%l@*@Y_4j}*K);KsK%NX-{S^1;pKwvrZ0_1jvt_N>)jUgiJam zGD|eV7#5u+ydX*mO*-nTtcfh}Lk(<0>NIp=Nr@y^sWSG2+1T^Kinkc8>LQ^{%XQ0R zZ|iMI)4hFB%IplSiQ_u6PV@w$Ua_OIW(73{!CKI(PF)V^T{#|nx^gR z*IAZgmqhfT5R5afSUqS%IseUKe@Rjo42Yp9Fc{U<)j>OMYI3D@?(xhKc&Sca`@#Ck z?B_;~M%63eQ&wKD4c#mrfiFC)sa9JPYE#PKm`SHbAA` zm)z_P9nJ%v<2=BS31e&^0)TEmHAkE>1b$sZNO*co;OY{UpntZGU++ee>MD5AsQ%#3 zl>w#IkUH6I2~SqZ%T_AR%n}RCz2U0qk>Ol7>m!@#6y)F07(FQG3`>lepqt2u`xdXw zD78Io!`te1I*`zln`Sm34LXd)Il+zo1dC#pvis;9UN)J=GO$VEwDU00Q)^LfQDISO zG4ykOVZq70j@Hc7G~FF&dA&zK-v*P6VjWu1rG@)vllb*6ex>h01Ebsd<`5N8GZ1DB zhiPDpo{Am}cf!9!u@umyLW^$G;L-(Qs-zr3L~ULs(z@9!=QZ$cSvFb2((iNcV>2-;%RS||xhj4Z-7>|dI9M2B+!xczP1GJN5ioAZPW)`c9QT9Q55*}b`!I!^M{y~)${Y}YSrd(#E8lW6o~o*HE*5( zT|NY7e0oH8M(c<8|Dq?6Qf)ULg^(Qa9sLtO7n}HTpZAi_m(z&Uhsg>wgp1&>rdTkU za+ROYdhudxo2Wnb57He)dsyG6rC6WD>hQIw@t%*9RCsO?-;KYy8LVx&tH56LkvAU3 zCDXaz7&LtRI(}Yf?XVC?3n?hbnhOzL)9N6r?V6!fTBjfHd2~=aID^JK*u%Cm6D$;G zKexrS3AA*PW-&86>b_7El$Av<9IFyGI^)E-u;E6Qj#%S#k5h~yGQNPpqVH92nWIux zn9DE&@wpxhJXGY^V>FKFKL@gZt7d*1P-@@o3PMt6FwIm{GAiAi|qdc@_Lj5{%$`%=J8ZQ@M!*QYS#u=9Z#u zQOvWLJd|hL>wk@NfrR%IHI&98P1|7DkitE!aMbs~cKwv%Zsqx^Z1Z5HK_bu1BL3xo zuM`+Nm9EIuE4ZH2s<`$~J=b~JH>=+R+^f&sKFWEy4D}5Cgk-(bFxyshYTy^B>0C*y zyqPM=!}$sptkH|h24=%pD&IIOpL@UnU4I!M?g^2K-@kw5e~##5qJEQkGJ`!~SUH)Q zf}~POu`FT^3#k&DcX=zYh3HOim3-X=1zSBGzi>ZXvcjTWMN@Nnc&DnrE~sqQFc1uP zbaEU7y%Ys*wu71oR5V#6NTtmqM@DGfEGA#Oodc-qVZ7uorxAIB>TF_JLQp04iWEn^^IMzclMR7ZUc6m~ z2NA~>F+R_0gM@sJj{Q-F^iCQwrv-wd?DyWuk4HLu7@-yig;rE~f~3;ckup!?-TE`J zB~Ffxeap=r-Dwu)<`q^Vz^@8@<(CFhC0Z4%7y3sK{MYYdgi_tgOk^^7Th!2Cd0%mO zlsT9?_yp3RF-W#lFF%zzMm@H>@cz1%xhrSczzl<=W}5#A?|Q!H(2Qq8v{QJkCEVIz zVg8xNFFVa{WOQ;n1#6}T0cUz-skFV6M5WSf1Bl2(faW9&3=C){SA2L49#GGJJ8Ak| zhcqGZzpI@uzq22OLT(>Gof=)5!hG1W*1135vR_K#kmni@WZSwlabW}Q1#@W(Sf0<9 z%Q6Idv3~j7WmY4e-w!xf3~Sql{;zLp(}kJ!92(ztQ|^lk_|tc{og&ySUFd=Ja_}jQ zpVkYOg8N0$=-yNxY1ev{nLOZ@+2lvPJn@x8d2#a0QCYZ%o8%?N-9Oo?#TDhIKb8XRdEMNTd*Aw0l_)6u8H!D^lZcz)em7{L0U zPZ(6b{T4-=FBG@{VPj&&6OOp$3T=?rL-+XI_t9t6oZe|)Idg%m*Ia0CMgD|z(r3Y1 zlkCPhmxzmP5tUgge#qT6%GjIlNjfJ3d12O66%xBF#=od+joBt(rA+U9IMspfF%t)oi<-YsW+m!V*}{G-T0^AD!Uy#Am;x`fT!czm%~G2+iAFev_I zR_-}J3deQRD0>B8`K?3#@(2L9#@DE|j@#bePLGcAk}j`~ef`b54}BJ8&7_qkqw&Gegb*C< zszGg#h^u++knWZBivLobV3Jicx7osX-lV{O(hwHnjXyeTSyV8VMrVnuW6;2O2wk*7 zk#ZUg#vI-njVf|L=mZs3+%TM2*1vZFba&fDL*m;?>uwFiwcj`>DaEPolB%@zIad_& zzZ|_hL_Y5Lyu@-}bu|JQS$t_UR|2arJxc0ecVIS`v;O%@<<`n`AeP}slif}!-9pIq zwewt~OIauelgLnd?ZF7^hhNa*5rE0Ctv9h$g1?nVmmlKs>0S0c$eXxS%3tJk5eRS37j9V7|OzFsS(s^#m8 zPMb)KNlayG&j);q1S_`-)#sfjr7>P7)qAmKOD4VO#p!Y~IXS>pCyDI!0>>|l{^#9% z^^%yF79&~`rDy;jUST?(xw`Wmg47I1ZTZRh+sI>AG5+$C?kJ~@lvVq9FJEot%H%Eo z1Q_TM!7*!FiAKH-{NQ`GM0#!HW>$a zo?oE#cPGe<9vtl*9>yLYTVt@8#{%s0Ry7=}s#uu&3e=dsL`Fe`9BQkhGgm}LDZTE`yYIKHTyuvZt{9Cu~eG9`1N|{R3KL+@uMfKm8FLD8YTv z{nc{w1zO}{XOs_(aL9=T_4IBwPo@8gZ6txrr&&17)3eC(XZpCGmz8~H$oX8Orl3EVoLT|1>kxhPFJd~4t`#Ng<+V-7&rxc9gW7i`Ud@85YCsvzfZ17O! z67{&Fw##35Qn2kC-Cs^0zE2{3-M`w+s#oNXoJlCNA7+U)qk5hM7q5wPvAmpn91;xn z`0vwikECLyR7nvTNBsDt#;c<52a519u3q$>L3A@%(so;$!#SUC;HfZ~fimM-CLoik zlJM|IaCC7xPz-5##x76SM<+dJ$=)?5VHTyNM8dQ!jBdDBH#05%SuiFtSO}9hh@vtY zZB#^Rp(Dkc=U|Ohq&@ZXCn-P%!o#5X&$wE@(f;LT5P*k*@Lu~b-QfpaBpxK{br=Z0bF^=7ZKM0M9UUFPQNkv^ ze4Q(uqQum4J33soJKV)^GdUn|^O)pyGg;7s#lndr3Y6{-Lc*Dti<$~H<;hYdEidFG zxVoC2n9?Lu$YU8uxf`yq9sj!OW~pFR)46pBXG}ZR4p29nz(&T2vjYd~(NBN}_4P&2 zdGnX}BNf`fM4)m-2F<#Dz4gD(pENHm7AVbv42z;xM~+Ef0WWyV|Nb@jfW~U5GOY(t zqBIFnw6=gVH!gX-)D%=e!bp_v|%5DWuvHG{Gxp(*HCC<@Lro{rU{4!8~oNNHB z!zUnQQtPy60@NywXW7kF6E*u3v$Z1d?dbK_l^~ux8BlyGm&*XKj`qt%wJ!oJ;6M6Q ze8sgd;`F)xbs~{;-=_{eHr|w&JdxFDhyFimGcxI~V<$8AQziqxPTa-irao}Vr1Bl` zc>#fK07%;e%Hatd4%>alOO4fFawg(B-=7}c00!5nGXR#+;wQCQg`v1Oh>i@9Ne&r5 zMAn3d3_H=kPVh+tcya=hSOh@+_J`*NzbnjtlBL#{4m8EOe$ZLKyN>BMlx^?RUSKu{ z{)61-bsaS9?tKqR{50ib5C9{ob#g@z_PM*dvyhcm|smH zjCvhk&wF>E0l+i{0R%Emj(0-8^(DOt{4gM}>?P~Z2{XU7-|_KgJ>M>H1{y^}rO?B< z<4XVG=Tkn#j4!9yt|9<{fSPsurpM*D8)-RTB@O^Oa)n&UsQCDJffw&u*(j-%8lRWv z)v9<}ElhAM8eO4ZF7cj{pSz*V{+GJ^QTTsh0)8R<_sISVS#$R2v=zQ`%!bHwVF^Gi z4h07?H)5cf<3p~5rIi)WjhD^JOP(WUx8E(t|HIx}2i3JS{lW(Vgdo8+xVt;S1B9S~ z;2Io)6Wj@|!9uX$?he7-g1b8ecZbbgoai~>x$k}c`@X8Tib}1$v)5WP)7`)B>F(*) zN^Y}IbDFC(Ppk!9ZV9la0|yY`9UF}>>k`ypz@eW(WND!`Es%bHZyJnOL!=jTdU}E0 zqwgy6Uj#`3U%V+}0L+fSy!qIh#d$n7VQjRZ6suYht#fE4)>Ock76=&(I9d87{SD>4 zX^!RO<k;xdX3@-%Ng;nzzsX3@e9EjWqU%}r(kn9BeeVn+ zaJycGMXS}N;?NPFQxyVAu+dY9S|JZQgO=O%+8&XvBoQ7Ry5#+PU}%$q&mzdBAAj0; zY7)x|i>16;nvs!VovGi0LM{adE{0A~vX6WxxtPxZ$bOLBGx2?S9)Ov-!o6b;6TeSt zFj>HA5^#$x33s6Xdnb}6JU$raW=U9PDaMur%RmpHNiMf=u)dmBD z5Q*n^w5NbUgnuG@&e$4WB;o`r5PMgJ~Ohj3iK)QDVY90*yy8Vkf z7X9wO1hNf-KgPuzJ#*vv^WJpai*fC~Jbd@=#xgE=DU_O9_L zKfWJ`12%edUP__={N=xH{Ow~SG&*3YdCoi2;odJle~j)gk2%1UJ|Sy$ydV4SSs0Nb zU{pxgYt|dT5C6AO{5jfVGJt{E3CHe8*Yc{dFzb0W)B2Z~NImpF2lVH_830Cb#=S!! zV)>*Ts*`T%M07DSxhth_#{d772ABun*{jqdPapmT3>Mh_WEG%m(vNdGbu}GzMGyrC z6GMB5|8lis4uYqO29}li4YMeuWs4=oJv*N7vbwH17Yz--;Uoefxd6N5v{LJ5awiu{ zTbr#h%-rWZE!l+_#iDxYoy9s!__E_dzOC$q; zM<5+y2S|NJ{D@U;rQtl>zMq#|JNf#lYW@1A+S}x+aZQXTqw^*5wZ8cNC+o9}#*rlT z=b}8x*7>zl559@-@b zqc?}DZ`(urIm}$0&P-M%KE;<*SH6B{&*vtQ`>;IyxC1>Wcd3InefVZ=Fy7&)Ro(g> z2eu<+8Yfp>k@boElA3MyIei_?+;G2NJlE(IqQj@$2B*;HLhm@xvMY@|mrZ9VKovW6 z_|z@vySVts2198Ru`9%Up(kj$#L(!oR;3W50Wg>>44}rSB{fI?^FqUcuSW3Lr_SmI2m^YlGEA@{WziGqb7SRpjJ)Q{pWRS5o=Nd=CZjNMR-e2~)sQv@GnDZ&K zc9i8%TteyDX6{m<8!hjuvP+$U!%&XsxEgJU6m9(645@f)!>-DKX->5^%Y5{TG~WAG zMG%Q?L5LJC8L;mxu`|~Yg~<>ZK9V+{)8T?t*9}-XK5%_Y6x6bi?O|!#k5XtV8~j)qbHW;9~+1D1LHi% zyFH{1cV3mZq^o;2gD_7RvK+5jSU7`F5Vk^gQ+W(}KzZY$!C@~`t9dJej9nDK=2VD` zUo9T)c6LVe2}2#tDZ$3Ltdc$Ob`xKF`mC~L^r#!PvBZ)mA)wdkRRbBz)2QN@Vh~;U zh%d99RA{k+Zvfks%5&#M>pS%FH;kgZ50x7Spc#ax2CA&@++s<(5aB)1>G6RzuitTS z>4Du@KHt<}k~1WLy&(zChYxQtx2##U(z0WeZj;`YmHVF!w1i(@-Xij~u+;T==dv~p z_fMlMFdB6&O|05)jjun(5I!5Q2)bPoU*b8KHZqgR~hcOufr4}EUE=7(_+S#;w-`7%wq z7@_roiR`EJ@Y4e!Tc}a36xEBGGl(edYo2U56V8gA+Uh~chqvWYSE~T%Ojy3l5s{QE z1_^R`%7jQgik}0NfcV~~&7|9=-BR5NP_bS#^98kWY4(uorTck8LhxS;OOMwy9I8Pn;SMkAj!l*Ca}ZP1 z14hVF91{^RY8egiOaGHy*73}EYwc-UOBK6}O47IoUto%uf_hQ=Yz?<7CGC6C$4wBK4jQE)UgQ>A_zg zQ{^_hf;JowT6})t<*%zYG(WeX!m4HFzj=}$3n0sS0K0WYRet_@U&T^%LwEtbvsBvW zOErA@U(|e|#^16WBI$%~1@@UpC_1MNRmnI(!$=N~_VDp0 zixy>m8@R|pjsWHKib~e-)(dmd&=nq2#mje0)i=ZWK z+Q+f^Z%-xMj(Y{yXghQh-Y9D9V6kr}@Jh~z(<60IwHZe-WapynanGZ;ZH-w3ju`M1 zENEjZaUCIng~`;n!R)M6Cm%8(>PB3Y@39P_!~jnSfq?a$9!MI2 z%nNwAT3JubCLGIpHb9tdzyEpzG~v>A`^>hV@Z9Z(#Q)_%1j~UfV%Z)fhBZ3>dB@rQ z`2%tg_ukhs!*wBQ^ra$W7Lav=^rU$a#_dR57PYe0yFRCQ)?(+U0~TAhw$ZRI z++Y3Gbn}%ZnN$$`R-wyFl9my(VtA)5hV_#|e?uiY(l6rRrYNi;L{sqd z#?H&xK)Uc1#D_ugj_`76teUxBj&qF&i0e36N0(gBg|rtFl42zFGew4BLX=t%`d>4Tp#pM-x7he=n1W zAAhBk(85OViD9n&>-;L=k81>6WI&?Ik4*~{KYu2tH9GAJtch_d9uQfA0_D(k@RPQ^ z&46P=ADw3VFf3--ODLD7jEEQJY^ExFv4HJ5foi$YJDS7jU+Ap->>YOc+%;bw%3l6C zi{)-N+L9p$>GFsnSN?~yuR0HGjbf;d&&}(VuZ_h{ZH&>vnnmsGPTvgW$oli$ST!xC zSn%f(L=J4`7kF3ab-yH(d?)Eo4+^|)5nfjHENl>L79Je7q_1IFzQUXJ8UF=cKd$(T zA+xkznY?VjTZnFD{>+ZAL(;7Zldp_SP1g(3WPA3>ZMs=t3H-&`PH^CUqL>FVa*)l(VP~=o?5tAM%$P>Xe=1tR$$RqO_0joS(E@AXHmyrw!v7$yQY5YHc=F9IqHLi72Zzn zoo~>*#?4>an;`|!!K>mv#fS0VdHN9-u+z>uQ^U{CI~;j(&H$rj>s7~tF_tgOU-#gO zZC9S>y(!{~r!GXy%%-&En4fuz&FOZw-1Rf@$^kDU&HJzih2eJ6hKv`j5grh@O#&+UGhpY zuNzhMTy;qcPcb>?kBs;^_4F1G9~?i;4^XJkAI2tARV6)G?6q|9xj*AG-7<}J=SvgHSAbo$#XbIdSj1=*g*@LeFgRj+Ia0i@wtC5M zg3zE8F+x&29l;>Q$AL+{c@WGQzS;?>Ezy?@qmoS+vBF-T9u;neW^hpw$skr>N#l+t zL26eq713Z&Xy1RMam_wA5CONkjha`HN@QGJm#XfRH8Xr1i3?};F3a5DE zbDu2Fu$na}OXD}Pbfe>1Dq3N*YN(S_^F=R(Z0O%i>>NHXbp5a-n{RU5Jmr~Vs0^8s z1oN`{`8tN$%?;(vn>nWw1kM)W_0zEp5b&08;1s8IL5~Ipx{uRRrO@HYM=K(le{Yk4 z3<1C+$g(}JO@|0*$*(o;^D{IFZpRMU&I<-ho3D@t*YKWF6iBfz01ab!5@efItN5=q zc#-gC6MutKVdSi_fe;`4p}0y{7e~eJl1fnd#Ia0YSvHYMuk4%~pWd`Hx=BEe7nFU# zpl1M0$8-SW$OR)!(!p|~-&0UkmEzY(m1s3&*o^pjjw5P=P1J;p?bIdJ0Z{h5|Ltcf zR`!nq#JO}+qX`!)d5VRWjd2Q#T@yZrv2+H&4rSwWqhD!qM1h7^;L%@R2QiA?^{FnNfn;r@^8yUf@h4-`hRc)O_*H%Bvd&Exd!F;0+eo8}vWrtutR zbhrSvv!%ed##uxP2mz1hdfsTZaIp!;nMdhBnmz+UTyk0+P7fnIBt2Zti%J^xLb2a;emHp3H{5ne(Pdc`5P071hk{tJ&e|SsNH3Q8)Gg zHn(xJFz|QEpfy^eF!!)v1I-3ruv7}#KOs%)ue;QSzo>-n;-W25jW(nXJFMZg7Ui#= z-sDwXikXtPke4UubdVI&U6HTutT`V(P<(Ot`He3w62_ej|GiLfQT!PjgLzypd3)U? zZ`JK?Y$)TPfSYJ|(`9%Pcq2+Q4YQIcNUFDh>dDrvr|`C5z?p{8YVyB6d;6>-Zqk zASst|TcYJ0u&t4R+~SNV1=4BGcD9hWx-ehMbTRo##LtKLY}Kwc>eu75FUzCGaCqF- zQ(%W3t?8$DH+lsXE})W!&h&odM*KpkyMUql%X(RpzvEMguZTB61Ezpi&!6Nt6nz&k z0ZH!4FmPtWnl7_Eq2*@_l*= z%ZPF&RN0Y{TFQ(d$U|SBw#pUpz>07;ztWG{OwGi^e2dL;y&_n3 znsZ+~l4hXD&;6pe1+8*^1nH%hQUJXb97Xeg6Zs#)-^hQLU1s>Y{Z5Vfl=2I`Be98v za`FQuH;qcua61akbG}uIUv`YBOC_$t+Kay-snJxWo*mbq4F`4jEt<5J{|u`>AWafL zHW=*&!yJg?%2AkS12lsPe41WH&Kp=A)U8|>ob z6Xc*}55>+;QO()aQ>UO=s3mlBOyT64tj?Va9Jrp?m(|!8&bw*0W<&(Cz?MTsB@(&Z zIBa~0!G)+y(K*kQI*~BIu(dUSetWWR7ktXh@+t2+7TA>;o2#tmUKe?r+X;dTUz*g$hwCN;(gX8)?7{`+*R{`!ABc>O zF3nkoPFx|G)=M!)UvE5UinRJ9uc*SH8Gm{Cm2Bzjh-t|NZCC=g++|>~mRvF?a0Fzv z=b7t)co>ab7Mi1)!R7G;UA=rnK!sU?F@4<$;3@Oq$(GKC(2a^s%cs48K3IK9g?WM1 z>{zcL87@b@yo@%1Vohn`#&(Z|atN?{WGM3VPZjW;A7r$_+4K}0k8Gw?ZugUqcQv9D z9-*`u8A(m{0S>bjcIo)U5Gp-WEEcZJ2)+>JJd$p;_pCBorrG#8wQlC#O z?Cw-6r20`GuIG7hcXp577_}XmJ&6gJKb;3(kLflpJgUM_$4q2lL`2 zcSEb%#)pNlqMH-WVy`&aKzz{=+6HB@_5~dar4Sl*x+wZFxtt7TVs@D$*4fPFluE+f z&aQRfB5XscT&BpCa@EH}6ZxR);Y_(plcv!e!M<9DVvwti;V8*%FMKf}uzyQBlsA!l zjSED70we|26S+uB%r!Upf$SEBVCA%NBV*(kUfQ9Ed2xf)!*d@jaEXK9!|lPn!sD}! znB?KcbV;}IEkiEa6d<#M5&?H)^G&F7cbUo^3bKomFbyaYaQTS3AfAd*5M;%XLnT<6ei4x(;Li;)@_J{8Mzdq zuBushRQ1SGGfsM#sC-sBDhEmX7!4s;&f`t982CzctkJ2WPq$VMXfUcy+4r3vEaj;w z4WOb)A?eix_PU4KjShcI(K;5*2-+B4=3Hb!SG*O?xLa z4KuxkXXJCWYaFKMk=%9yi$R9I4Q6%rD#28BoWa3>rzqrj7eu=^Lpi5v-qCKcxk+-6 zP9IRJD`uizh|6`7=wxvH)RYS_Z|C)aP7-UAN)7T*fV&~|#9-^W^Vf*V}0^xLS0@x zz_Ua!pSBHyv)$;}v2hrou-yC1ToE%rNI3T(CR0Rwe!^*=uy|)UuEtnzUXX9&f$)bq zUp(}N8;P{c^>jI9%3X54kUPgrx=ONbXhZzZR2h`HV1ZlA@EzS zD`p!u0elH6Uv91q+G5ZXWqX~j$BAZVi!HB)7Xfep6{u4RabV}XxITz}W@22a5$iNx zw-jV9HrYL2EBq`o?^jO5ubDrf1$aEk8w>yE@_ci2d1yZ}w81nOge)Gs21$gXWA!@G z3ZbQ1F1N;Tlb6V2)y)8Yh|I(;`{rHa1mn$lPz+S?E?NwW*t1*|fTgc33ThOPHE#lx z7%1y53|OvpUPltKob@3D%Hb!5_g$?PK08eC+?ilJaE-cfo~dE2O-qhVDX(BVz;j_) zwg{0Fwd?6!0u8R0#}2?C!Q-~Yy6^z4<8}@Bh#$d1jK1n%J79Ub-cZCghQITi_q6A$ z_Wo3M4ov&oz1yniDioqQxt>Mx`=98P5U0ll`9WLet(Sp=VP!_UL$RrH-K6!0dX{v9 z`&vHkQD0)QIP|YCsfW&7rznf>NgHZ~pwi}f`)^(5&MyYTq2Mi&DHgz%OR#M2NIf;W zKF-z~d)vesTP_Aatu#uanfaBfV~yv8J1kqSO&t><{9}5lV^q~8G!?E9M@L3`cpshR zCX1ikJVD=ty>P-n6BQ(B!;v~tK4@fqjYgBKfEU2h^T8n0BNx2xp?Wsv0F^63oZ%%m2if9 z+lG^!C_{OC96D;5h5XWPjpyGMQGBk;6O~3n22df-xyR;E8W#gU=!6+1CQXKp`vr zu45>hV$&1qwMs(rxx~Fh%i+a* zP5kNE>?~-in^uKLabNEs*K_Ic>$S>Or_9@^kJmujKyiHnW6&rDa@^-Hf}|V0SKbOk zgE=o;j2|NSpG{{n?QYIlI;~|_F_|T3W&)5)a(mejSg9xO%Dpnt?c`KSa=Z4%+q;rQ zNmp*vVg52ii6eVpt?nQL@5o&Etz#LOxk_&r5XKB2NqQ-xobCQV#l2hZ&7Lpc2(m~_ z%PUB@j*MU6@#Bhr4mc9|fgSD1zthSY-hwN*kk(yb6H(d{G>^aFt5+B1PYyy9IQ6?l_TdztVnU$#iB_DY%IU7n>mXSvWQsfZ8x%0-_N3j&mQBli{MaGGntR4mVQsPDLh>43h+&X{%`0p>kF8T00;My65EN zgh@TqEQyc-5&X2=Kr{5zM)4}*VcKWRVK=x||6dW`0Y$J}Y?cDC_zX(}Am^z)WRN9n zj*8RvX%^m!DkP!HIh*MRP))poySsdSET6RQQtKzm!Og8QSvmE6?vS!Xbk12i5A!!~ zLd`=c$6z>&H#6ii1BPd4WHX0DHm-0bmi0YYTp;$0J}s7w2t3rH3_}!sXRa-mGFd$~ z7BD|YXLVk54p_u!>9=79p5K$_#;~F0T_NPj`tXT|DyEo zAHDCkd0ztae+7zu!Qj1Dm@fe3`b^`lfy81tUyqn0q z7VOVQ{`s!|oyh-E-&&b*oUnYNB;3?!`z-8{B%{&S1}%_ROc z`R`XRlmIv9qXy3S?-@$zL<{8SyYs8*{Uz`EcopLP9e+sa5rNXi@f-r5v9Vu(;yDHGeYnqyg`J!Lf~UI4Df>uu9s280 z^-o`|cU%5|S`R_sNCp~J`{}pg1~GuuU}^8P9_kTqcUP<3`^ZBS!|`6Wu@NB2!aFLk zIqBuO30UfW*nFqSkD)m}t@w3T|0pRGIV5WGf=s6c0CVX49rNobs{QJy;9M~0$y!}y2e%AXNb~W z<7%28KmW%SvK6U)a#zO50GZ|9AjdY>+n45Q}!T@+N*5*zEV-32JJqykjUZ< zE9?{Rvl)@kxZ^*P+gAbQl2toL-}n8Dp#TQ7xuaKN?dC@=`3g1b^7ui{)qCtCV3~qm zd5oV3+220RQE-Mw_XbjSuqL+*CptBJ<(q}51x1n}`oD9w4Bg#aRuh(fCxMTRKg$>GG79 zM{{*UR>xUErhLSspV;lmJO9IpVl6Pu4!(l3uG3c}x^LvuYg0W4X~jt74c-Z%)8!Nt ze3+V=T0o+kE}a#JnM@71>dPI}C{Jo*PBJ&oVx5CJ&8f;tB+MiwCuW8iek zBC!&TOYC&q-d*93vhpQ{8Azc7oDM(%(xLq6^ZLX?Ogt(z$a{dO@i}6V#mh{e!0S@{ z)>0?EQEa-BBpIccPS0nrM~R&9SdCvHa+HjtGXyBmV?WYYBmVvjIB8%!Ek`b^Io~WA ziqHJ^!z9pXMn#?ABB#&~o>j~VKF7W6%9pCkUNUZ^K=OH?mlFmR?CJX{W=H6k$NM)= z8<5wf29G)#&(mdCzQ@3#3r3WyfvUhz0_+jO!@NUHri&cIW`#;Ef9|UItM3f0O1q$= zZ!sgS_mIsnBDPAfP?PXs46k%u7K71h#IzA=vWF%oDS@La0%)lW?@?!c-NU+ovy}f3 zroTUS3lP9Tc%OAOBTP-YlvrT108EjKIl0B4hUyd6y{N(|`a=*2?hbdDF#Ub56QBrx zfcq>*7dk_U&1q2_DNyvwS27G;fwScOOH2kLwaDgf@Oze?-yc>N7SkKQqLkpJQX7SH z1;U~?fIM?CJiat6>c8ywosXy&~HTmY} zO3U=!A>qaB^2|>p{Mw6jrTYQWcY6|C__ZGekD6}glY%GBa_a*>xaj*TfBT3^f;a~Y zn`pYuAcI<8v2NjVZsKwmYkd8Z`O}YD0DmvC7RTKIsK|i?}K|!hgj!;T>tpp zvqh^*-am7#?#3=t06ud>HEtJj^nH`&Q4b7lGZ6dg`?i~31AIoeE6_jq2QltjT;Mwj zBe2S}{*Z`!2mKGu`tcwSLNp*tB<&gp=RZ91ck=!H6OSR_)Xb}?Jp_aQ;auh~sx-a_ z4i{m|x;_s1_1*vd3Mi=u;NQ-fZ|VQ@RJ5Mo9a*L1dw5T%?#FEZIme_j;Et%%tCq=o zjhx?v{^wvB(g5oEL?*twPl*2Tk=)%TGX>o7;_7B``~E@PLlsQfI~%>bzv(+n-Sd-w zQH8=3xFh`rWMO-+NZdCtxNHCl30Kz_{-5uNtN`v9a&_bQen9&FVLm>ufZXQyht?K9 zq~m{_$_rHh&5RoO;_o{&^OFq!AdL_eu={_0!O3xtBD(*MbRZg#RW^^`cTfKCzgG79 z$Nv?R{}q#eFx`Kan*Z;Li40?!=r4Ka$A@s9j|R%v6dQC{dLlI&s6uOI@`;XFyT4e|FNxxR41f-&h0>3`WIDOU_5`B?P|VrW{iR!9U$f}ZT5kl;T?ThB4t=7I^DOZ{$NW^oz#Ky)x z#On{a-L2VMt8*!UKiiomubg+S3BqQj8rP@X`f>|&%9PmH+VWKQ#ccqpJ?s$zEuYBT z&Rg&g=Xl3`%I!k&9Lcu$$jhTz)D3P6oW!Rvbjwvc9TKNchxn2rkbb-% zd&$N9@kF_?JA@A?iQ^e!4%WavXTLmiz#NQ^M?7pjVF1Pq%Q1E4M%WAPddl-2&jPL^ zm{(NOcO8q@7py_h_0((qV$NwV9b!AB!@vG<0( z4;vq2F1OE@+k>~{(73O#fd;kD`n%$9Zuc=4OKp4$hZ9W`a2x0sMW)AN6NDz34@^0o zad{24V9!?X$V#0`XH8lmx`AlE=j&V>f|+8aTC$9)$pT2QN~pQYN=;Nu zXvhpDn>=mCr=nJ{uu7zf&eI+b9v;q?jqhwv?Kr}40+Ht$M_l%Kcp8$|0%3w7=8NHi z4f-Vsq4J(3%c=uJ^ZLnoAfLc(r7m4rnmjh0_8`v(uING3B2KGOin`b52eLq0n=^|z zHd7dIUWdK+Izq6UsM!0Zo3GI=s6*#yq~ALI?F=DUUjCqf#y}TrmKUaU7sE-8_(5tX|cOpUjkEJnftIO zB&_bnP&(F_{$7dJb`O0+JTgNMs(uMlGiaZ4%AljteV!&?Tk&~mq6@l3u)=%Tj^4NN z7V5{78q){)o{28E7(?4zef1Ah(2j>fnH?eop8WH}*=L0w3$|)eSse(-+wvEG2n{ z-FWnaI#DXV;>Wj)Z}eA~P8hgpmGTv_v9b9Gt(=&2RBHM0Tu$dX$^vzZ1zKp-s$}Ne zZe4(4QZ$AQ5>KY_6=8Ym6H4U|9^oOWp7@KCq*}W#MT>n2-0yCo z(2(~_-Ble=(%^*7ru6peCsTsg?)=mK(+q-HJL*xvHoB;m*4L@%iHU z;IhFc>^Q(uj#`9#PNtgNYd}k`KFICngY8L`3?2Q=(&g#$v(4Dy$&eyJ|4B^#FY|;@+e$8L<>1eOiZDI<{@NHNyi7hu9AG+ zo>FG+{;XLcrY*K53X+6OHb3 z&LA`-i{G(&wtcRde1g-KX60x4O;?w`Y;t~&!EA#*Y%zC;=YRkC2>j*gcLp?oXenZ- zm1D#T5aT?8wIwcN|Voy)6cL= zyv24R+smaZosVgOBAcj{Fl*t3Fk3L&WM@mT;U2%53f<~}NR94tk>ewaX4=}z#gj9$ z8fF_o_@L1WpW!+J->=vr`BCebXqVq*|FfsqshWLJ(@Kt_2mYl=0fei4NG;6u{&gxy zEiD=5+De|UcHALB>(ud@KOXoG21f2!N{pMiGW^6xh!XY}a7>@x1O+dyAW|=te(5m) zat*Aa2*QnwjSoqRdZMae$Nx%>1)dBc)z^QxSme&$oD6+!MMx)DV@Z4d`1lYD&LaVX zKEx-g3!eU%$38{qqb&Vvs+VpBytfQK&35mj`&sN6TEhb&JXV1w)9aM4NS-m-`3lW=A?8;&9p*tdBYF=*>r?1m> zF+<2SgAQE~*qyQNKhcFs9N*XaifWIA6w&pXVfu3^+!+oyUywi@iSsEzS^1Gu<3#V5 zt2YlfI}8v$xzTsogIG@DxUO=kr+G*CBJryl(LYT~cqEsGfTna$lR-c&DErG@Vch3c zw#P!w832hoTuaTY3}Tc;ZxE{-Z>~_*TLKwCAcm7O#n5a7&Tvlj6NEM=LX)I93F-EB zGyUDX;cX$T_Are)_T{H!$OK@&{yItcJzkO_nMM5d=>509Mgr)dwI0q|oa0Rg1H@>S zuR;uj+j_G013nKO2sGeyfJ%MQWgjt{n6MwwxAt>wekRs2(EUrD)YQ?^3PksiB`{VnYucCa$l6?pjbnyn8Y+G6QDAO8zqWXRD zM(PbQ3teSzwuwcVt|ZO62SORDS8Y5YQ#ia{wUUHdhYwG%S7$%ul-1|lOeoYApr2FK zdWhnAe%*3{Pyzb#`Srt-Wvo*ox+Vw2^QORu1$=R{pzio98NOCy;01~WYDWIz2`Ah; zu{IH@y?r($oXO1?zKEr~ z{GCGd;Hhf%(#$&@+Td&`UrQpP zq^|l-Os*tv5YHWA)O^&vj=@X6T$YXwYjRuMeYf)faY6uRK=I~5c}v-&GyFtifUREg z)hcRv4j;Sepj-gt?I8GYL{#8zy6?N5xhTcZf=*Ud=Nh{B376NIgB)f1<#p3&)iT`Ue*`DO?0^=qBB-WUfRQL~ zA5u5-zP`~07pm9Ea9`Be0pCCy$zJNPl)K}9SQA!=^Sc* zkOJeOOvih5Fbi3XS*zr50q0dki$Nsq#|^ajH1PI6KEw^WTy(Htc;tdcXjlhr_)sUh z<2wxe4F*+@f_63E?p9a?epgDr)AhNQ7c`u_+KNd?w3yhsL$#9cI5oWnpViCEVJNbd zT8VDX{@yk&FEI_Ft(6pw33mp4pkWzMENSwwmsqZ1;in+#Wyk3%%-GMgShH5AZ-P8MSGh$`O8W1i;nA}v~#UwDY1 z{qemmOf6In^R(*-LNPLjy%=J;fUXXN*_xW1sQzxPZ7G6Y7JkGPKr2Fqmi5X~*UZ^3 zCtpm5@zmH`12kG`IWWMv{Y)foCb#3bfRuFH^&}9Wn+DEgZY_{81!>mh7kv78?OnUQ z2cU^e3&Hv9ojn=aF1F#L9ssyfh(FI=jXbf)d3ebK!zfemOvE0l&K1fLsRiz>=D@=` znA2LvYn&{2jfQttgM0C&huBu*fmJYx%p9M-5pkA210znUtd$S?mT!IWR<7W#D!i3-p;!+(T=}#-WGGQ%AxUg z0g@8s2ZsLy^S>`7bivnHZS_Omu<7;L7+`ssYP6a9kj`}wJj*H`SKQ5;t`1oyEb=_d zNtQ?=kU*>5YYfSrHk!{nt3)`FPUM}n_EVeidH4kSIv!9k(MF-nqBr(#tMb<#j)+se zlnigDYzofC$@~Hd7><^BpeqhS`n)!T_$`pMRg)XI~ABS-6Av0yg4F+&@)3T=iQ~rjd&qqa$*X+D`uR) zu^}!?EFO)%NrpLxntqHY3a%6O*BSx^19qUDk7=wS#1}h6tI`GR-p|$zl!=CLQf()g z2Qn2i_OIXWGH+$3nC7)ex)Tn~JgpK2C*l8}{*oYkHgBjg_GK!^2OO)jqS?O7Y9l3q zRRMPw|BAL3orvMeO#cb!5m2#6fVnl|`NHOL(FfC69D`jC8=_k*OK?=oU=D4C|M6YF z|A0Z^5oQ>o74cgERYEgHjkoQ_)8sE|8hOwIl>_|`wFSBKdn^ls1_A|B<%VG?7`RyD zm0|Da(|;}O_m7R3#9Fl*{0rd80Z64m#rjg`Yy6#2(a{F9RZ4zAD!>6@yinCMh%-po zkd`{8CHl-xKf%y@hqQpuAgNXJ=&rHTb0f z=TDd+3ssZWfC>R!^JgR+LUw;5Vj-e4PDqhzgBOASGi`obFP%ttIW>sl7j7w$&xlBU zA7`W$Gm_HwelGKK91{~GVich_!(pLDMxvxURTh6)-1$Y>m6HBU&$Rl$7{cSFkkC6c zLHMNO5NR?S8;Ls| zQS!z>w(hGgcYt0si41Em`Kb6!$*XAZK$S9>W?Vf>YYwBWQE z9htjV)h(w}8asR6ss2~1fdcf=OD5RsdGpd*?uCXcC82p;%zFT0!7!Jjn@RWN4?Jxe*X#79MWZy>MdlwEtQ*EEREi>3BjhO0jQ-I{w5PCxy|TrTK<+F6i=e zzYb2@M^ijV5uMYqqSyb`h+0S)-UBslCA*IL+g5if!%@-UF>D|A@c|_GNts118X_)@|QWb&MfdYCBH(-S;~&#iuYwr2cL!RrdXPG4;a!nWZ(d2b-iQYTwy(`@&E^pM z${o($PLp1pk*jkfQJDhGJU_T+)zzt|3}yj^lAqI{M-&v$QF~!O#ladxvdTzCN@_GT zoUup$@34kMt5tYkA%c=FpiTDh9AWwJRMjlG8?zRUjq;tlvG0n9lTL|g?)2&_4xA5d zff%ZD{sc^(jh%i$1T3D76nLR>x{*!8TV#+T8vY0NzqVQT!p%Rv^k5Wgq@)uxq%jB( zkvklCz&QEYdx?lV`k<_Ui_l?vuqw}GnwAL$>RpL$7jjomPeerp8>3(<08c$}NWvm5 z4LwbZaY>N>%_{-BLLL0ZKQK1V)N*i8#itW{wua?;Dk2Br>Y{&%isVI?UUa4&Jz^v+ z-~(&P+KC<;Q;6;$@{(nKq)igBuo_@w0WmZJW%o3c=)Ylb0uM%Eg6td==pX{7F4W)= zeCh6YXB}z9mJ+Nn!B}J-28m+8aSOnI0qMBQxqEYcag?5sAtDve@p{zV)x%51I9aAs zMfB^_#h;HW9bo zmFT)TYnF?`ecU^upsZ}pivNug3K6*dSQA)zAyNU(%Yd;#y_4R+z1S z`C!0yc1&Fn`lp=j{}TTx(IMf4BF%B!QtgX0=qLm|_!t(!h`=ObF-owtwu*{YT6~^S zoRQ_3#c{IToMvdPg zH|>poRC2L8QSwV^74q7=fc=@w;!nn_lR9dS5R_q*|MXMvtf9|1re`ZyA+%I40TYJ= zePO*j!t`XNl_4$;J~jAJin7$Rc>VTW-!%Uyc8x%(5(`r_uKe;sc(g75I@b^ zYCfFX?vsEJ8f_<&7QB)g651;Yh|Z_t>~N5_whTi%DSezDR%s9d)&L+zO+_ey6hhf9 z;eoLIf?7pj)b~`&|5v9swquSMB8COoOXzJpH8Hndaw4p%j#IV$M{D-=JogLn@;d{K zR?I^FrMJVRqXQUz+>V{+w~I8uC@N6ay?0H^jZHBHn~d?*d((!OTlkhahGo7n=c}8S{g35{d%CPSqnBvq81f;^#GLL=V{ns zZnZV?hY+ZIX}3xD{{KbP1=19uhX5EOq)^^S;-9rQV?PS`9t>o=v{U1M>_}*+Xi=W` zE6fu{-0?Q&lTkQ3Q(IU|mlWfTQ&8w;K4$*#enb(vtjaUWM#qX^5GW~~x{5y}%r_hQ zJoYi%>o=M1`JC~U!>N$$Lw`eoiUxFU967*5!|`6COufmCqvsl!37*vlNm*LT2wd&u z41%UhHEZaY6|Jqyrgw%Kiz-sAFT1RaIBR_Kc7RQF$vbpF_Wv8w|NR9&dl85{L^|)V z2$y520b{{P>C(Y_7@QwcS4weswgU;Borj%CWRmgK#?7txb1`7IiU{+!Ssjr$!0Fhp z#|->7_*RiqH%P+w`3uf@MDfAqR`No3w#?TtDv}bRK2+>mZ}3D`8-qeMO zND->8j{03l(ucDTA_KHyT1QK9vy7I&xI`GAm*+$P%wbIG`}q9y$TfsYle@aPwWYZT zm--rI!UO~77X5Yh+wZieL@!{AiyitjC;%9Nu}AGYJ5@o}pjkCcUfmeL(w#@yBZd7)LuuB_2^0n?jPRd%(8G>><}QE=nx>UvF5Y;rQ!41tsRMv><0Q2afzc;+Ke) z=YgDz`i0d4)_phK;)m5|E~ zSUhIx?5b@zd46$GHaaIoZWI;~`JQZ2GEN~$%x~PJak51o6~KR@ z5U+g6-$eEd(Hn|N8BD=(!H90xKt?h93Z-5tS`yR#F=0v>gU-lMhfI0s7qFQr0|!*9dHrRwS=+W!;714_b7X^VidXMe8U0>Hz{L+-%|dZj~Q@5 z``;Zrb$$pN>dPj(h)4$cRF0j)8Th!{QuYsVP!0~=+3jyN0%5R`0?=`6+$R=x(K_z! zSbdYI8DuDx&o7njHS1@dAls8?(rykhzL2^uVI&R6)PF9hA^~oB(jL2ydO6V(RfUD5 zoXhy}?ib59mHA{OtM?;%^NsnSRs`{;R-BUeWEgmcehqzeA3gL*$)=G?T))TCY3zby zc3F^da(yw;(KWIne=#HK0f=72mtAj-GN3OMDqqn9LW3SYwqujR;PuO0xgyX(o4o|x zgqnGNw`RUIyeIieAOw6QF66s3@@We5{%+OTzNPg~AgN7sY{Tjy ztNb{0+^B^zqi*uC2tZqCjjc4mM`K#gv&x&%kcAP7=`~%P$&HAR=cO+ssU%fWIYjgW zglMG~d@#Pt70lV67A<8$bno^-t-{_jH@r9DyuQ6KulRf-yx>U&tAL$8O3*GxXNmF^fx*qGB#ubIM@0@6+l$={bfVd(N z0WO-+>d1lansF^g}iG^K)sC2=7-qn0As6jHX5p4TtZc7g%G2d93_yY7KB0u9ynG7m!>j2zn)uK=rxt$FBuSTDP z-R#XD^v|Fm@!{fH8#RePt&I@WD-^C7cPSSR+gNw^LGr^0jfB?i$3pH|*~HYPL{@+R zY7N`(Vq$rV`rE0l2W>X zv?__|Svqf#afA(kAyH=2<|Ew##=dzGkyK19u9@5~7hwh|)Hc$E756{}0*plQZc!yJ zDz-3`ThVHnrz+v%)AL@(Of%jriWRQhACE+=5RxBQtmg33JXJ)e`KQxbgHg|ctXEFm zvRSE1jWGv6ASjN8SmAzoSht#@B~-hS0O80rbd#K-rlx`=UjEE3!=ATz!Gt zlFHz6hlmJ0cWQ?ACyKm4xB6k$9!x(`SY?L?H<_dgAtIU8ok9?(pkV4f-c0t?>&2~o zBG9b;BnfnWv28^6-FMe?8FzJtUslRSAJ$3x-UxkN^3J-w{Dj%G1% zA~#9K5q8qw81{3*XB4cp=I+2#$74Y^V(J(^^aPhH`3-g)`|&U8w_D$kf_00HS6SQT z`o8-XEs|oT*tY?lo|W!pWNjMR3yhO$`*y1bi!D-Wa+X|AgfU+2aY>x`635PZ*HI7h z#(mAeZ6XOBj+CN8>jfyW!a8<_>^r9q{y%CTfw7?bE+2p?b@Xv@+@qJmHSe0S8Lv!N z@j0{3YZZ#mrapnc{P)3nAR!;(E;pl}6E))fGJ1a;8KM0jg6MzlfJ7lVtKw3@+i&u7 zb=n|x^<*a(7p?Z0-e$y|O(ws;AG*T;o~vE38bEwj>Z9T}?{sxGFA6t<6`(!BB2dE; zF;Jt{^M0(9oH;L*XHqxnckR`c)RUO=>}1H?4ya>FQB4&Zr+f|mc~F~V!cv}ld)SQ$ zJwI-mvBNXVX(Khzh163}(`D@oOuJ_RK+7JGi;EPI^tw@FM!@&Thbq0gH#^*{<<6R= zX2KnHjj{fhWXG<7X0lt-crL>Kw9Id|*cbfgJVS^B>O@RGHfaVE;236prl!(~MpqK2 z&>8>DBl-P6{R;;E6hCQ8h?OLYmbQXxpRThm+T0!u-h4n1DW`OHE~0BbSlzfuLKuQx z{K}mePvpf(`R<(!%7?27rP~vIeL8V*_^^^bD#DXp6r4gOc+khdr+`xnmn?#}orTNA zq(w8B^^btH^M%Bupz>$JBPF9VTMA@GJCrAnLji)*Jsjxo7i2I7ZS>7aV1D{*Z!;`r z07u8G_jG=opR&^{XL}I-2-H&=pce|Iuj_5=pe4*Yhe9JxBfNlkWV){O>&t)EWYZr4 z8q=&FlVN|f;^$VeV6fY?r*$P>Si$BsZ-no~O3Gn>be!#P!`ojQ3ofR>8qbCf02fMaU`D^Yw|^jawsI5;;S;`7}G_+)e7PqIoZK z>>nAv`ZJX2poP@uqf+g6H=+!PGxpnoud%syuN&R~mN-}v;)Lj6f4|h68T)Pc=nh&! zx;eUOk8-5DT2+(@*gnz`NAJ5E2mB@FuZ$a($8r21?@@t-SU~~X(V*1xAKP5*U%4n; zeVFMScjmnpGZyE*5}F@}^^a@SWB)uPvWae*gg-zYEr6l9b~eVlcy;ZwJgoDZq+ILJ-TP}|Y5iboMcx|Gz^D1jCgGx+7)SlM;E2=fBw!_540*zaj=a8|i2 z*wUr{4t>U9LR{EM;)T;nt7?pR`>Po3Pr~&ccekkM^|T{ZQ)Xb(O?Z5ZWyUiVt4&&? zcYn@rnV_I?3uoS3_`w^}Au|SwkkE?ZQVy}eydfKdPWulTf!zyDT-p({9T(X!tRqhv z7Y~pFgS-bV&qA}Io-kyJi6Us;(_CRgxEtoHol1;^7zQ$>$oQ11)SVb~&IM@-bHZW{D5hq2_Y;r1jnNbT= zdX{#LBmJW5Bxfz^WH->i(;7lC76iz(!&NeH>|YVo03SIN`rOoG{4MoctO2~biauMH zXOR(VL4|EK25i_eb2LeRAyLU?sgy#wzjpJ#$?-oQfO4z6wi!Y{lt6KfQv`3gs7iwg zY&6piXaB=QArp$1m1dHL|J4k+iiZ3_D0W{~SC{&Be8Yae&3bn|Z_+FrAO^+wu^V9k ziiu)tJj57xeNuB{Y}TH@LJ@_JG7RXjyw&=e&?+4CpritAYq-aA=s2;cbC4}0DJi3s zmM}39ucD}-l$I9n(C~UqN2x02$nvYF?x0hMYV?wx-Nz=zpBtnx&rA%QULqz} zX_v!cVo>L2rVXc;G)g&n_^SuA36GMZZ{(C=V7nTCnDr%?y5xR zVSsdo`^xSyED{xW9!aF}z!sVN8EW$NqV_C6_L=}B-^9v_4L?GWLh4dzZclz6m0b0+ zXJ6~T2Qh&7v#^|^Ue(Y$hD1wJ&OQ*kZ>(+WMf>K^fa6wm<4N*BOG!Xwr)Uo=B&!hH zty>!=IP6rgLI?+B7V3oyRc(rhUTvA0_2IWDC{_NXO~iWZ;di znyVGC2UX2~Z}z1Bk{bnJ>b*-RYJ!txY=+!CX^@bX4b=mgkU+k*NjqixzyMDQy$j(SgZ3*Lx$-1YhFg%*I`7t`k*rQ5rG+OM=a>zATPDXKiyMMGjkmI$T6sp4b?pw-? zZt@0td6o#&7KX59Ym-zGJX%|_5 zRk_*=riXsdQ>(RBOlHul`l(gofa_zzC7SZAAwqTx6-Q&a^P(t#x|b8G`6yJ?7qeUE zLW~Co2Iv&QC^)GXtd4xF+xV8IZ=VyA6r(N~$5|S^j=xmr*?pn;PR)iJbK{c3p>5Q0Ea>r?C+Yr=7*)!E4z*(ncg4O# z8^z}|wEF=9^{AUCKJ|AU$8V&+-b+UT8K9*8)?>=!cpvx*HvYUXfH_PevoJuLbCp@$ zdyG+C55pxh(7XN7Swzp5o^w5K+Va8SNV03cjFDc*UiB+pn3&8bmC}i^*!Uz&j4!pJ z6m)ynSh4P0^{~RkfIWidTDqz(bSPs>-BAA8lgZboZRv6Lw%Sv@Ie3AWGU~lC4fdOF z>SWGSIovI{x8BtBT}yaYa|qDa!g!&%yY}C~7{jE3nW6UwpKDGKV zQ0~k0A@rppr4~o~$|jfFc+wV@QQt$#at6C*fK~$Z6)}?0_#Z%4dJD&`JZD*j3+ zAdc5JJc&!Bb1aY+X_FX*7aXaps%q+OBNFn>k+D>N8hZ+1%Mz1#8)vr6@+A5KmVi9= zHLcXVLn1fU!&-D8{?_jRf{1yuP&S0ohTrcC0@A5rRI4kk;tqMLM5A5s;{xRFP0g)p zoMcqY0?noMgiIG?(flE@)B~j!Cw^ZCHW2DT|1n@}k{N_Z0Up#=rZWu0uJ58*UySk4 z`r-NmmjboZyb|5I$(eYdPoLsQCL}`mE#=VcM$Ap`3Ej+fMSLE}+5J*I=( z1sF!4%sb{xC@AYPQdq=-%}i(M`C4jf7#LDjRhwKzuEXSMOBv^k_*iT0bT*?j0*2vh zt;AzI3&>h;A5e|Pjbmzf@;Sl;Y0kg^ANOOVk^4<18tT#RacvWx2!(N%UOi+=wJd(r zJEO31wbILGK8mfKodaidDR1vq=kN=!QeG_lUrX7Vy&r7=;Vtb_lM|rp3djwRw5XWU zA~^-W?r*PuC{e)}iWPm2BxC##$oBntsOv1Cy56+@Cl4A+nO&MtqUQBOBnL$jCTE5dBanztqUlDSUEkaYMy6gi0|7*wR3f zNw0S3G1%ewBi>(Mk~hx30|?{dF|EG|&E?`e-eED*U8!}7{=vc$)DO4@OrG0Fu#d~% zU6U{X`}RG+ZjPU7sJ$)C0y{YsY~HkkGzjCbPkLjXJzeZg(cy%wphtx?1_s26)EZA_ z^TdOy?G(jk%J_3hM(k?vyhiIpR{-fVFMuAme@3H7`>ul`39%n_b?GuU2K?K zd#6><%1smH=TUNYk}Ntd8lOCrFkx2$P0i*Ii>Ln`Fu7ISchCGWl3Y^8?)`vEFf#M| z)#t{X2b;fgs38JHCI@lx7Wf*Q>B30_#9g)~OIKd*0}jSc%V{r31O3Ds<&uIoOk&4( zN?AjxXm;$mRX-4IoiD$N3a#@P{d!l66dzYJdNwG>Daw|P>HF)jE98YeWu9*g=*;u8 z$XXyds}-e}4NlF1JS2p&yMVl(Z@ ziH(krN_ueu)j1F(w=`^YEAZU&*p6Yo2FPkr^q_X=2(xZQeYWsx%2IGrV!@H|TU9Ns z1u_;E5mMt<++vrYr(Rwi9zUPb_{-dbdC({80 zWWq?&I4Ok`HaAA2(Av^n4Jjxxd59BYfDEx8DkVmMyySoAz=U5XHJ2e*IN!%BZK^=FhnOA5r>7~R@ ze}qHL$Vj}S&m?|*Xq~vx%j0QPa@orgm&71!WunbyL|p7mW|WpsTJ$C)6n>-l{M^C9 z$|{k8zt5dvW*AD_EW1DRTGBWZDt|m@ga^3(DcK=CAw>Pg3yz^Z&LDk-21renaH*2Z zhVdw}BBf#$t3MW9O4miofM9MrCS`x$J`%6 z?B`o704$u!>g_PQjB<4xkLi(va(_)(R{60D&l~c3&7%uGV`unKZ4f z1dGYZ>$A7QCR0;W%RNK|`PL}5dg~hE z{gD=npDlr0Ulr0TPC7F@hOjk>7z-~{A-LLbslq@n_v%*n8bs^Mr=D8h-IVt@QZkO! zn%Az!N%Zu1g{*{MxNk4mt-fd)B7{->po}d(p4HQ^A!Gz0I6_^LJQ-?Nz4Gb4B zJ1a9iZGvjS&V5>a>ZPu+nJ{0IdA&9-byN?QL1&(B zNrrxhtbo8tp~eZCi&2LB&HL?zrM z1(SaRpm`bQZuOHhSwXoyOZkmgGEV?QTYrU|>^dTI0j@^*tQW7<&jK)}2#GB`2Q#%p zPpS`)44+T2>MfwXqicOs6Y;s*Xm~nZN!Q&O|4N1}Gq58-l%#4Nn9-~<78fr`7~VfZ zu(Ei}@-s@2>~mX{z39Zbk<-FZt=)U{b8XW6F7{!Eh9cd?5KpTc#>Y(H^Je+0QeJL?wC5{>zVJ*~#=#%`Qx%@Zt|5vU*1epwd^gMM}S zk!{8!%sm(gpO&t{M}R#3|8=&ViU6iF##%Qz_zW#5H&|)Fux~1WW665b6eqfA5QrGu zU6n5ls5&{5xO!+-Ml74&STLJtfJkp0$~LjrjkSKhsK2NvFj2TN^Fh+bN9nkMRsoaV zVYIN8t$5S#-pFAWdEp0J7JLq^^>RFLjr6s+TdYNIdGEH!aL-Q_8D1|~g~g5Z@5z>i zyJkR<0#-GNd$HxJL^^#xf$ZaC&^HiUL8^W$CA)^Q+ou7UzNlZYE=SUBwL(-0M=Pz= z78cUdS4!r1UTQyahY&b9Ikg?T<%LnK3Jxl1ITll|B|Xxi^5ZCF?_ZL&Sm6#rw#FcF zF6gaPVAJ88Qtnc|hNM_iqCUqMoGFsSC80OFfr+|Dh>K(V`U-_0AxBVbgTR5#UIh%c zPD0b|Env~1pdbnE#{=nh@$~|Mw6M7o{D&+9Fckozah$plUlz1H0c1-+LC5aYSym)_ z>dn%r9y%Pf$Io}c-A|9w?jK-ymJ!uu&vG2*g5fp-h!FciDiE8kj8|=aFp`t?@q-sh zHD;Mm3VEE*yPGdewue4{*S)%;Zam3i;PzQk9g4D-H<%qsbaDx$(~HNvF?c!}s-dBX zJB&Zlp1zN@(LT?xu}22S*=3*{5{Go`e!f2YLi>n1#m$qjJwh8l?|5#qys+`9rg_OE zsR0iB8WRU_NB39dQ)}j4XOb4JuN#5-W^=yro2N%MjBbvA)4-YEGJt(5o_6)}*g~9; zp1*(`jW;k|r~@n3|3mK2_{{2d>x%twE6y;UH#5)92KgKMM2QnT$MTeD6IoUXqE)Y| z3!U@Dc=MGo(kSB$+7_wmC4xTRc0~ZH_K#7SKPJTvVt^X~->(1%6&vzDn-uPIp>)J& z_ueSJ^oEa%Gl1>#1E8t4yb6%Z-1<-dbB6O&HkbXslZt{FtbsTDS?) zkCxg(uW$HTB+<&SjF(`>JMksorCc5w3Kpqg^JiEho%S*2F`c#^B?X1Rm4Fk}q}4Jh z0IK~(sOXtk4x;u`Eqf8QP0&XU)KK=~Sk2I-uBt^eZ?5h4!YMqhG|}An{R{oXB>#l+#qVO`{78ty>CzC%FR1O&GST-+#t$P*^zzM33?3lg zb?rqVbm{uofd+Z;R@q<^9uMh{Mh~D21JHquIRl4DAYuR7mwk|d-?_;QR&2>aVt=^F zFk_Tzr^jaV;mn326q%fITfO#|zCA266B@4Fh{@FK%dMLWd>{{NJ zsXvgsZWPKlVKF=Mm+^}yZ;z#1jt@%GQW^Jd1oJfv{z}L;BbA!1nxVU7D^iIu1IQFQ zEH2fBwUabGl%DI81vshCzfmX~Bu#klE7*bfp80Lc)< z$CQ4G@4}m(=S%)4jT2f!}p}AuS&E zB*sGO+ge0K62nD5mQ92dc|2x7P6^)3`^~W4b<`MlD=s9WWaz2en8TH_PJ(yBn25#q zokOWXxFTgjq8Zbc`js#iW+TI)PKU_m4OYNbRTW+ceh07FGRN4x57S=|A77bK-=uS| z)>;exx?b*?TxG1eiqPT1I2E&>}4^JGcbM?#v>6Y@w0J-^Re^^}scDNWytw zwhEjc>?m~21q$Fc@e_bEKn_Zz|7&T#B88$gV82S{!+;|ZI31a>jt~3>Ck;!PBuVxz zVcU_ayv%!{P^mxBseot8?fmOu)N$hVh)c;^g)ja>Yt(ou8rExq{G&`T5{Ga9!>nz5 z5fV4~mZ9jJTbkmKmBG7#>b_cv)PD<*?1MC;N$!QyEUh2Go3IFlxxmFPV7bfq+(SzQ zPX8}FqeOsw6G=Ikx$PV+36&xVB*A$fHTE&5VnO^VIYU@)YKf^E*p;_q&pYqq^5v zeQeZl#BWz-=%qD_R{YK{COdP0ylm@IwVQ^8haod_1lc?s#?d}WiOQQVkQWZN;iHJnf5AG!?@}gU=I!~=#@3OyROCCW#=f^*EbaiA+mGK@>_e83tS}C~EwnJQx7ECi^&aebd zs(!I9-=^9MOlIzEdt?$uI>;qnzodZ%5Kv~ZNiWB;pHhTn9R@#y5$Qp3l&uQR;4{i~ zNSPmic82%6saCxGu$^|ldC;B%cgvfwYRWH&@7b-iK-HQq^+&M;!~5{k&%fx9RHej3 zE19F{fV*WfV7$+v$oa3R=eLABh7zR1v7>dn==iKVlI2sSvZ?OeeuzCM(8AD}KQ3M) zXtmXhS1-l3!UN>rq~_2JDYuZCs?Kfbf#D%7iJM;rBlEbczj_sVls5@6qsArI{ zm)#eXD0_#q{_Aj%)q(;bu!Su@qZdAM2d@-xPr`D3!e<+A8I?d>OJxNd3!7B{HE0^) z>Pt}xTOm?ocE4H9DDK2CNS`v-+>41-SI_%G1=RCX&-$=Y5j6mQB$<7_H!Ya0*PmPy z;cScCFQG|u1n^7(Jc!abR~b#W_T1|_J7c%JKVB4VOgJgEBCVxe5w+0PDBon@%QQtO zQD<8%q^RW9Dcrj>DZmE!49-;LmpSYjG+Y0uF6`CV!Nu#dHl{o&9~!uZY+*?1lP#>joa;>>Ab8fHWZ}i2bkp8>(4`}ivhW{ zFMdA~2*Pn?r6gGD_Er8q!Y~?9ze)^`0{U?}Sl(+(IJPqJUVI)Gly;m!V;v(2_Icdg z-V#Ovi}-}<@5aseA6IFEo=B;Y)V|&=5NvQ=I9;~7ky0}2%kr#ePNW{jYE*+x^;rt< zjjkamFJ|ASx=?O_-J_`in$k6UR`!K4-iQz~ePX-4pI%KI+|gqsdHgbChuT@B`%R?q zW2xLVyERcXb=HAj$3V$PmPlwyeI(k;2mk*Nzh^;R)m!d*dT2WsoTge81USS8orj% zQy?u9C3xQt1^)bn2kVK9MXv{VWRLq29-y>87}8m-B*yjG^gF#q3V2Js7h)$vo8;(-pxUy2kv^F}nUgCHg)nvTNB{>TMeN|NLhl?$eewN;E5-bmZUCg)6w(bxj$54>b z&Qk?bat^GKu~)Yk6zb@>2hq`K(LrYOqlZ!$PayL$@GgM&DyETSyeI9$U{wszcM}@~ z%YEO>(fGC_=85FlVCCyv;$^2v=b2owc2WdCh1(IWr}C;7DY@QfT9 zc?YTzD`s<<0;yLk%XDJ+fB19_id!-Nkv~36GeR|C?9+NMz5-imqJz@m-DWqVIx*+2 zJq5GleKamXwuvzg`K{6A?bgFIv=sQe)!R8bwLlN)0iBfO@D-JzO? z^Ad#+)rQTd^!+kUwDZAY=aC=HdxVD{U6~T6J}4Oi{`w?dv3zRx%e9olRnEva9-r0+ z#7H3k7Sx*PnKtf1t(X9Qa|d>dR$rIlX1PXrMRT?&;gicMJap4e_a*E-F1Z*#Gzorb zE8dCh)9)4uR14{TV4U%kw~L_UZLuJ$KEF%Q=8givV7NlQnihN>g*MfWk3=P!-0v$! zl1|V|0pGowu&^JUMrFsFWHarmXZDkQS(_}2nj=1!hJ(GCx3j3Df7`eGq=gJz=JBP! zs%MwI=NzQH*N>$RQmwWcc#~Sxe$H@eNp~kDot+h*ej zlxYc@CWe2nrzHse?Te}3SrCX$I#rq9V{R4O&a=qqsRKSZxXj)ksA7lDuj zvKKysc(-oz51{oMCxw9Qo*j8sppKF4UkJ=E3)#F%+u3B4d?E*Yvbw#{strv~l@#Rh7SY&bOmGpxhJvF6BmZ&z_hk10;b77F?J;lbSA!cui@1jC}_tw@IV z-r)54Ph(G@1|zMhc*zM-{ux0=3`Q)4^?;tmSuOIO2)jO$+ zfguAKxNexaI~=(3t5+9zeev>{7KH0u=0@uY&ep$TzqMuDS9th?OTUL7G88fXTTnK9U@e5hf-eTw(cxJ}!?L_k>ae^g| z_awZbQ)ex7qc=fKs=lY^@=S*e=;X|Ie;@c}LM+Jcj`i-?96~P?P^-n2lstr5VW%~V zUzhom34#?$sHk!Tt82t0xL{X8tPIDBqL6XAH)lpe$nyk8IQP~giY?G%bCxb1A5oQ0 zq}?Q}exOD(k&?}IwzY{%Ad6v?^nGpO;YZO>RW0;>Y6v#Atp6##XDLL*nH`?_%bJ>= zkHvaGoDgud>tN@-`wy1-fl>*pd5r}>VrQeR&+yxSPY)GdyFUuw6#LYWdC}e7ec!CY z$qwVu+*RMS7poHVxJIk9~7ir1^zk%r)gQ>~`wd)=fo9!FhUQl4R1SU${T5zQj4u|sFwrI4iHU$)Ueh1N z5z!G9JG6rYV`6%?*yQ$2)aqJ_v366b?2g{!IWTjc2Pr8l0yNq`pPdf&_9W;8zciFE zD%9@aybAmBo&>*Hc+l0WFJZnpK=-zGn?K^BF+B=A)8M zJXlvri8KU>2)rFLydxm6n28{;8YgRG{q}7$Y-9rn0P0S^!Ug}pDJvdAR{VJvUwdgkXfh}C1ZjDGz>csJn zctj1Jb%F_8%bIeSqre%hS|r24xG~F&s9kn^_|o`GetqCg`4T zCLONCY$wv;cF~(p{u}cC{qR~8!VzABSj4V~zd7N2CVfR*s1gH?a@rH5F`|8hn7=X~ z7au|t+28YSK`SqvK5$Y__uiohkS0&%r>@nB((vS9SSFB!-?Cr~v;-6v) zHepwf;?Zcd9E!RlK${28I`kA2@K+{NxnL(h`YY6|F^WRLPyVh9p7z<6hfdBGAM1

?&l`?)PSs6qBZG|C?o59A+DTr*+nDr;1{z)A-3b#IM-zpj3meuBR30DnxX<(Wz!!SAzAJY`=fj-O z#a<^-wu2SmM_G>PvgD)@Gf>~g&)`Zc)!X9!rFEF~S*}Vzj0}sgiygmv>k#=X0E&t% z6?HK&7&Hl~ln!Q3JB%%O+V4n`PY;*PGW^+C@)~8JjCF>hUEnOSMeRO3SoOe;@W+2r z&lm1Vk@(clohbRUhvAT{;L;_1iN$=n=Ap&?TGVRd!c0Kx2)FMx>=d57c!dLIq%PE= zd<$*wqNOK0(`1c8#Vd{f(KzOB;K8&FkIfu7nIDGHc{q#P)o<)yCPhx|>dmas+R?De zSqm=M)Wdi>oVPY3=EI%sifZ_H>yW*4)*ZUJ)4cN@klSM=RXU*0V9?;-e(CtDIDp@v zuCDQZzi~(IONEznRbZzsYK*vZ+bAY11sd{eVUs}f*Z!@weP45hftP!rG%9i|-JHsb zu$BE}TB0H+r@2l#m=qqV^yw98zU+^9clpd&rv}RDLA1H0M#s8m*Q?No%42lwAJLi_ z;NY5LpNSCTu${lpLkIbLh;Co|1~G;d=PJ6)YT2yiD{c4pXGyi+KS=6gz}NC%Tm4k+ zc-1a`^dxT~lcB;{;4`mQFRjP()ROpmz*PQ>FEvh3ndX&p;Wv|DS^6we<%{BhAZ3QQ zSL~hzT}0E6e(&&B0xLgR9|(>JzBwej1N?SUCg?J_q;et5@gEe#?P;N*`6wD;54^Pp z+TIt=E^XT;scU5=7Z%cP*5;+I*H08&TgV_zVGzSlv8rgyLY0pmhs+&2RnHx|8h3xDq&I~HJ9_uX29yN?bK8vEKxag z^J8<(N(_$XDN(N)UL4qMHLgIyra>_J=Ey#j#9CJ7tqBlWTH@llN{v8*;~+5&qZFrK z=q&doRVlHTzyeSD|!2I?Yny z6sEGRkU+gKBSoy=YRdsqHV>%7w-qF{o#JEkl(*?O>!fKGg0XYsknMaNl}#RQl<)1^ z!#a}ow*L)e%zggVrdqyr2d8CMCw=2!(ei3P4*xqko%l04r#(>5?58Y(U6td1t499O z`xKxf$A~9A#~11o9BOCi%dz{I#4vDkrES_-6}p7xb8m*Y-^&+l=H5>y48kj~^Vh>* zQggf3=?k2x8>++6YIq(^H#}U=zBNC}q#Ny%$k|42LF%p*3@Kv%DX*PO2W+(MO z-w#`(4qwJk^DFLLq&`*DeByzfS)g$^`OBRyQvK0>VO7D@RP5%d!}sEA~mz zM<|=Z4}SRJ=`)3fKLOvXR7aneO4H&BnGi$KhJ|n6XlP9+I}1ouis1Dltm!KK_|yT1 zk4{_x!QVk^mlF9+pp%GMlb6x0Ty=qA>Vwg&LIJg%QamD7#LTIfI6pO<=hR>vO>BYk zbJT0zO}E3ZK5GZ+PUL>wKJE16 zL~*jOW>9~tzE9$}->`!^7)cp|7cwB|%K+EskgWO@Q_D&`tdWNt@J73qf8LPeKrJC1 zay-DezOgvuVoB zQmIq_yXL(3u{}21rrCUMy(BBxUFSzc=@9hVh=EdhD$fC5*~cuM_Dx3`fo87noD6n9 zjV3T0E9zSu`Ri|r}eUg6BqQ0CXf7hY}HeT${g)p?S^nDNs=Pl9S4H z<;#21t>bp0n6EoYAyeN!SPlv$-L2T}Hynn4_S)RHt9V6H?Y1W#9^v6<_@feT%JO5) z$@iPw#bdbsNSr+%eslS_hlBQVmAG*=jNu=UpFQX%p8Zj)_Ad@R3EQ`r@plZ`M&IjZ z3efWXisFPZoyOU_(H0jcxea-4Q|Xp%HXX#Q3wnljz|9x`FOv-c^*f&7 zEml9bvf5xK?X#LZBM(SSv}mus-nochwh~6fjKEWgh9lhd6aGxJ{N9BAMt$#MA%_;3 z?NlpG`{E64i6ysL5K+zd5PnI~ND|3$12r_BUt!J>5gvz#+dqAx&tOzbj%LM0n+I)VvZnoegn`2-#;%>~P6dDHG3T_-gRVgwkb0CTC$hWJU*q3T7|K&3&z&fbukyc9g*QoGis?Kc2NLJ?J=S z*Da20r*kvDvQ8T4;JIJS93Ju7wb^Y78LpNM*fb}-CaZdIK@%3ARNEdd+OrsH%x=js zIoBrGWShzo{&w)dhFM2XPYCGUiaEG{fiRIfJC#ti zh}c;^Z_Tt$JelTfDaY?39oB?8I67M2Ss)HTFcaL)0*YEljrjh8x|EBMbih@l?J4R*$8gz>$ z1cJMJXxufp1!yF=yE_DT4+PgV?(Xhx!QI^@KyZRPw=;K=Ip558p7U>a|KMTw`&QMe zRjaBjS;C-K0zX4%=PoOWM33^w<@@3S9d_0kyu~+W&8c98;>OD#hcacKT(1aKVCp|) zOn7ZbA*Og-b)~eXvmb32V2(s*+nO`-I32%l8_#S|AG6zvpGFj!NRY& zL{=9R4PUbvHAOkAa+fH<36?Az{Avwc3OmL3ENFu%u5jvCEnX5$`@3JY977)Z@vGGS zn)-G)hg9C?r42nSq4y4g5KrX$w+F*&;~GsIB?YuOTXWjnW^TGFwU!=qLYj?l(O9_p z?W4T316dsRvy4~JZa%gzxBMt)!-Osjv9uL5vs9FX%_3Ht(Mul-^#-wMjj?gxzYihc zHru^FDk&;0t@Q`~bZr&W^LHX;s$TZHZXCxC2I~^c*CiIOtHXagiFcHcaOZG4Ig+HqrMb~NxN3>!Z(`*uuy!|px;N@oJmCm;t(|kT`w}eegiv8Z@SJEt5Ak> z^>)T#k{}H-S+`NH7%;sf+n)h4%2bZLiwZu-3o=OB`}=o$1&i>dbH;C4OFt*PXf+~b z)MD%$2@{*pe6heV9g`s?Y*-e%LPy(@XYJiX=xH4WE!Ef-fvC~r!s;<8(4A`i{oyL``d*FKuU7ODA&HIQ8$r=aa7T4BhT zvvtax`$3G+7GCUtBrZ8P1HE%`5YDlty{K9Mdg$NE7iIW z10~fj-;wskYbEsyMNBZqvHi_cvPr_y1}pT}!9=S|b^Y~Dx`DqmkG61ZQf;MYllt)> zYWc_=1Va&o7uh9<1NlXsA>&~(-LT63*__yRc5^AI%FbjqAi4lRYU;lCBSX27Z6E`g zNA^4SZw&i;#KNU;Ck#R=N%Xs=#dfa^E{F%4?ATPNdR=}uLp6xkHwZl`He__Dy|%UR zg`^lChsrXLhodd}A%k@k*^;Bhh(iaTpSrQdF24k~h-U_Uws83Vllb@4BqTem5Xj%8 zwlJ8~wr*o&8i)2?sF)nM6no-M0Qb9guMnog@;Wd+GH2<(QM7+T{IF^&AVAgebNV8; z_A>bKv8Xw{S^88JF`A-fd|S;BJ}*THhtt%0DM3zIIqqn&rvKc)pUM1857HME3!MRR zZwHWcz0-wd_2=$f3ct&>Q&W}UQ?y3#l<}^4Do{u%oc(IH_O_=zR~Nz#kthF+m8I4u zQNQIePL(4^_I&$^{VFn}IG4mh^h~wUhPaw zI{IorJt#5DCiELjF>yjD!kZZJow<5g&7E&z357>fGaZy5dd))Nue%?1NN7Lk-;g{U zjpDPdEP(TWyVL11WcpTl9nQ01ekeTb(R|ain(Mw*)T9lf%SJ<+z%Fa z$0z;JioKPQzMOlJtk3fCN~H(Jd*oKSy5_k_SS1{Jn2KY^%1^8w-eh1rCfA>RlCWuM zsT-|(>?u;N{iuGo5Y5;)*3IY7k=tG7EY`)>Z-CadYwTWszwUE~k4U*Nq$ zIF)8NSnk+Mnl&?ZR-G!ZjV8Z)={ihu+bipsJQ{01^+|`$XkHCknuKKznzKDp8>tMh z^~q?00lBq}NIxj?JNYgi5$_q2v1@-kx8@s^nDSo%^PZ(cqo zB5KoCbOO`exNp)B?2QKRnxMsv*4t~%dpWFK?TY=v<8nBZa>Qe-Eq@rJ`e5akH(LT| zJUSQ2nDGOJhL+0r0`ooC+WH;WcdFNQkG01wV!pmNRt``4qd0RH5(J7m(8zo%FnvMq z!x6RWTT9fA=(~gcCKHv|;c4veR;brIk;i?sR-Dq(r($)Y zRO_f8#HpmV(Kn;dK8`TkxP6G9q-^M@xw2-0bHkg}1{?(s1@ ziRFP6xVPrFbNY(<+G$))nTcRt`j#qv<=&$-s`LY!nNQ-6Qo!tW4*-eNpn9(|)*Sn( z2PhU@UO?wZYU$OyF_nT6)`KE{W33P&ER}^PJ^V&msxR+b*2DQWV2b8hf0X~DhPY>c zpusuH-0-!>v`hRi!PPI`3mqhw#i6{Sxb7_24w?>QbszpAhc_&~zoMCEN%_vYp#5TQ z)eJ5~_|CfPuq7QB7c4uY5b<71&4o{A;hM7Q-L#%`pE9~Yq=9Y{z4U5*n~z=*J^ktj z;WHQ_oh$9f?V=ZB9^$q&Z$ERI!M^{X{k4dk8dKE?>;B6= zsQwG}=w4V}@o?XIYIJ&P<>;)nk-%K|%(j zRCE{8o8p3W$N@dwVk;1!(14j_5cSz&3rAo^uv58k&=I^FIt9k|%POmcRz9$YCR&5F zdM-~Tb|xA$hYkcc`}rcB-FXsj0Tk7Yn#HHr4!jv4RPXFRfSmqz_JYVTh2%M?0i-{Z zxO6*9&ldp$`)k_CKMP6b!noTz%Q~Qq)=7}5L-bZt*(%RC|PVHR-~pxn@BHA*S1Nz*vz#yXV4YOGfVqGV{#o^tuT1v@mMu?Bod*7 zsZFx^RH(|{L$=%Zc_pg2!KFryW#p_cJQ3(DU(H80vW&hza{P*$Dks07>7i4*GsE$E zFN$|P-h@x?#%amM@^)jwoH7?*G|yRia}zKzW7GjHlcX8a^5TZZyO!A~CD|w$CMpGzS8F!I#wA4c&1inLQa8-- zh^rnG<1oRUF4Uwz97OQ^Z5ndK)q`Gvku7+>KdeB|LLQv%gsa z-eShLh^;{Ex^kMK@2PXNt0BPx3u@v@lLsi$k#+0tL)7sJ=c!OQYwT#~B^3J3i(26f z1YZCyRdkn}^2CMx#4dcHET~ZpRM893BF9C`f(vo(7d0d7ehtEqD!jx;2ThV=RS-RYn|Yq;R5(781_-d6dfjj&p^QR(H)j z;#dsk&n!O13BA>Xib6xH#&urq)%C8t);#;eiR$l>1-x$X&u(R%rO@Whe45#`OJ~!FuhJ^U_U6yO?~H z%&CB;S<`NE>$Osn`S`d}Cx_qc$RD4er1td)iyEcULB52lmz>F8pgYK=b@!7|F}5Tv z@*`bvcbXbs^?dB}00wp%`*?^Y{}E<_)pbC`ybj_vqil(*pD*NF2H({d0{{ZtCcB-T ziVo-&Kb<8HL-%<|$@6CU5aD(7Sl$nxz6zhF7dB3Aov+dF+0}(D<8RiKDle-v`4Ty) z?Z)F09&E??6wkU(Z93_1$rXSP>aKreDrM6@r7QR# z1>t-q)2e620Mz{93-m=8_QvCZBvZZ~!3skQv58epm#Qz%!AX~%>dVi4u!)>^kF1oCOSbDrbO^QjZir^nFA&iasGFO5D4Q?)$rZ&=l7cj?P=Y6(aqDVYG~H5! zPbIL;tbL-;Hl(o0f;|Jq6yTblf&+SV@$VTx>U1k_0-8 zyKHVrIwH;zrF&6?t3Jz=eNKamdQ0;fbEW7iOEzSn(A{XuyQqx5N41@uaQ*IDV}|%s zoKtgSKElU>0yX%~0g4>MeCXvbi=0sTgumQ4x{lZ@EP2`#xF-x8jULu+ebOH}J{ zsSL`^v?|FKg_>n>BKA0dqBMYNQAHyo4JHM-OR^=eSxJaD zun4#i%mWR~GSQ9V`~b9=2w{`Y1TbI+I@2p2(-<|;kh^-Wx8dx*X*VG3R~WVJ?A;uc zyF;NW^9>PQAe6E}An?oN8*NP%ILT3YdGg_IfBw(8!C_>;>9q4BG{p{MK3D}{E|5sH z#~xKg&Iocb^d}%y$C-VMPS+oHNAc_4zJ|DYV=xsD`fMcLn|RjI>CBJ$?GbksXKlg6?vfuv&ERvU&p zgz{upT850XNe1m~bK;AYJ5KXkT+x|rWdMu2#kR1)>W?>kM=@sykp)c7yVzDv8T{vd z7{2l$6$CLoeAVw;n;v>G+UG7-{&?%ZF{f`j+CYy%NNAX7DDxK2^S7u+zw5fs*Kawj z!ofIE2;ld_99hXQ%MwFg%c%|@eylny3;@D;z6@eoQ!lq9L?B~wg<%_V9DlkAv^}Kc zEyT}2!9~Q64+58h-?qQ3)-6;Ci-_WVC$eqw{ONT!zpteKb-0oy|3-{#tJB7ZkX^(R zeoQJjyj2JLhahlOi$v33KhHP^8j9{A7I)5bcdU%>9DRrvHawXjnkWH zW5frj;$LoO5r9QdfZzr_x1t&*QZFwX4X*Pq@`*$D{aQ$;2IG&~)!xZKV7AD}FE4D% znPc0SW((vy3k`Uc5FC+;#Le7kj~WB*8AL*-Py&>p!?QyzFKxq$ zGq`$}!j$hwz<<<%E7o-th(nuFJ@Th&HsPT48Wj0^7661ta(#|^&jge~Ssz|sa9tdt zn2JV>dmDJ6ZgO&$9rab=a`B1KyD9$tDeb=83wcxzO6U$4sanQ-Dp7jTYh|jFK0kTg zx|BnA-L>aj`5k)JcD<733C-|D<$~=ruQ03qr%|Pf8Vv>am(G;N#*?=rg}loCCs6hL zPR>jY3+5Ar<#R+DS7~F_S2Wr{Ar7f0giD)ogbT~DZsNIB#_@L8Z!bl&dpL}ML^sEj zqF!oC+>?J5w!R9oLvo{InYkC?>^{v9wu3X*p&QcMRhuq=!9YH_hfdi#^&vTjch8p? znR<^8FaZXxm#X@unv!;)G`y&{@V@NKbeEZGH_iJd93~@DBYc?H9zPj%(<5J=Hn~f+ zm#6p{GacUSK7MYdoAKzTUv3Oz)jsw(kif+4wVmvR#f@r4YdwzSW@9Vw4|IZcG0*y{ zcrq%&ulVVgXE}9SO@0_szKK~SH|ge?G+@tj|H@%q_u~U~(vzkmpIIbOT<D`apR!@-;;!H2225=0nCf|sLDuFv?B9);J-ZGm;^>4LWSgyzC(P-$3V?;&(5QPFs zVW0qjl@K4_LkIt#z2!Xes23vHfOXWEz;YOiVKKvtZ}ew4`DoLZ;u#;Y zap|MsnORVycocHJU5<0^@mG-S_zlIULZ1$fUTCGoUY?gwaJCCqS8a%RkWQzr=h&KP z&DH$&ruK)O^vl<7MRR1x> zOGaO(pmk|f9Oa#;o}$^X%1@`mT>IfmvP#1&gIN+Qn#UF^(W$BST1f%`a{3rZOgplm zH=`|95*EGGT&=Cgu#d(UIfH|gmv)?ZJuIk$T1+S1>`3o{+n@T~ZCjIyQ?{G{E0cxa zPKUa0tM|9OKVd2TPyunQh|50jMgQb5=zZ?>+G04yw2Lvp#4%sKuw?jwf@7Z1mmt&N zv|pDtz7Xm58=U;%1-w4IQ`gGuw|2qEXdVsHS9Z3W9A*soxl*$~2R7WKxeP`|qjl#N zBrHS24%;V;(%(9Kyx!yG7VAxr zGp~VlNTm#u(E*|wKIf*N^RLJj1nAurghh>sVzn@^jr~KDWZpp_4~dCcucD?3o^dLn z;b1^2LB=w|6K;R(H(VR2v2`8{z&TA0Z&l;hBI3|m>k$_96tsh?x+l19@=BC@FJG@C z(sK@aPe#-Pyo*8GDxY+xW0<-ZX~cjJ4dYl;_5uwb_S9B$smZmVkh}5KXvI!RQ~V)R zFKK=$$MdFB!prw|F*< zgJF|~M_U;U2|#yphR}IJpt?Fj#zU(BP|I;c5aZ^FQ`}BIL)VzymKTQiMGI1vp9rl= z)__QA?=`>sFr=1)6=qi!mVZ}Su>W$;lAPc7p-d#DjMpNrHwD>QUR% z9k_WRHG&$49dFoIxUW{{P*+8Toq&{-*&}ArvM(YXa;l+~leAd98k#v^yP5Cw(J{2y z-F}nJ)9ffnepa{yV$s?6?hgrXFPll?3Tbod1yvCC9@7#>n+27XMz;4r*hVwPqt?e9 zuS!Abv8=uIX)yeN_Bt-)m0^~e(!Gc1-Kdo5ypG9<^C#rsD{)IR7y?m;LR1wO3-q3) z2!35-fH%t_&3XJ*>5}BAuwe<78D8(lX*t+I2wXzd_Lg_Xu1~#6P)0GsrY^O_(Sr@W zt7=d34L(6p3@_bQQJim$?yMRx-XeuD|Rw@1!~h&T3hv0c1=z1ZeR7uX(M_bAQ6X# z9W$aHFA()I_R?!eO3G^LaiQVdLdG+MWH)iqLGU-I!Nvd^u5Ekca^w}Zz6p1%Q2S+G zzyE+rysuq21xR2$86~qG6i0l;c{Z0KkCEVS*WOE`a5VsdN*mGBC+*QY1XAzQZxmCM z2T?OknT&d9-#B0%W#Bax*R`cLTGs0BG^(hGK?sKk<{<3Ch7jX_{IaD64)@LPGLa$k zpmwOJsk$2nTtAuJztUWqBRPnJhLOh&Yg~Y7p2&mO$-B#=ifMhozYZ&58#F>R(b>N1 z0WhF(Sn?ri#A(Q}`0?I05)y%tAc-h;c)+l}PfS*URVBgi@ZI^7z~Fh{!U{{N zmNFypYW#LLdVE8C`5TeTQSx9z$ZQ`q_0IZo2PCxSb$+f(ysh18ggR{a%)#W$5Eu1q zUQKz9^-`%&+(8#Jps^W6(pexup}hvusm*CMmdC0f#SanSdKX4-#Y9YCv6wLR#4B6e zDb9KYt+>NiV$ohEYf)6#?$iM$!+qf*p6hp9s3CGTp>n2-$0d?A$eQgAOncDGb;P`A zj+PfIalz@SQ||tlWy`Y#&+dQE`Tc850yD>C@1!OCl_t_qF=E&-&MZ?tre5H7&%hZa z|4z_)28xPV1dWb681bR1T$Xyr1AM=U=AKG;p*%Wg)8=+4}W`Pq^9~cpf`=(nBTWd1~U`AAw(9MoSef`e<;|1n_AmpRajh&J&J%7e=FRw6cg=44 z+tFm=yj+*KOhDdrPlOW1hl~P#w*44tb!FFTA-ZldXiPP7X!=uUijc+_);i)A0&6l& z$*H{>mmZc;Ik<0TuV|911BGN^Q_XvJpT0RxgJ$Q-aUNKM(&D4%)cvmcxY&&K?LMv( zj8N1Lu0Mh4$9hb8-x)5$^6ET7w9VCI;nRgF*Z)ReiB1gc{jdqV(M(1{fomSr7s%1r zqx$?tvwH++ux3(OtJc5`68R;1EBmOB8B_ZJY^QcximN zJw4lybv)T&Nh8@I8kp3jB=Tj)J#V30tK>2OGkIS=D`L^n7|Gi>|KuI{IWC4vv+>h) zXH!SUGL{4O{OK-HPMI=H*TA*1yGQ<^yW=Ns*05!=z)#z(AU-&QAbCr>W&*0nxGld; z?3^KiRx#x&ls^{ONqY8Bcav8~rKHG)wa)pFg(1nP9g*3o7(PC7$8DW_-Bi2i-LOlE ztDYpm;JG)b+wEppAt6&wSYiwuby*s4u-bP&o9PQemCvY<*aT--Rd)k;nihGDpP$b6 zU&8e7NUiqwk*o#whYJ}9ta$1UHc|o)-)nB6aGD_y)kO$Vrcc98fNKZ$_cvBvlW*!Q z*viRf4OHw!gIZM!R0^Vny)5q&Hn-4SuUCwpKY9qg6&n=dZ%E|oC_Hi39GrJ~8vS4D zMn8}tXxJ-5!)izKBmtbZnWl&E;B#E_q_hTej1I7QPK@5<;p<_pMFsJoTBNsVIHi!+ zN0FOr#L&GbR^ug_=X5z z0~`Lsc8_saYjpt5F+QUK09lJcbp;?Vg5~(wTS!MICKo$gM8d#cEs$=e(E>wVCzwDD z-YLVwhZN439K2b*uxr=r)#$7qO!+5%zJ>A?%*DLvcyKCo%K9#MMBA(F&|$2`s-S9- zE<^V)gAv(%rN|#)L+=S$pu+gEypL0-`5}O)1yS+!wOGLm@e22awG>@S(o_^Ez%asl zOp=sgX}uL*8Ma+=NX4pl0kQ?c^B-c$}`N(QX z9tKe-?6=@{7(?X2Et-o@x~!}ApXh~H22Bv}(gEAaTHMd1R9AV}uV%4iI}^S)kK)?p z*A-TE2=$okcY!*yV!z9dJerQ+dSg+8tlih)HT=2iZ{ zk5x?6PwUndIra5(6if>6q9#gJptT?qsoIqmKIVW44w68XqJ`%^&Gp(I4GBFelm%9d zx8In-AoVi(^AZWo*WPGcD2K1uWBqfTqW)g5A>VwsUEvQW=0}b2rKo+o+ye|KLXFg9 z483WDim~Vw&irbN!!3PA@jU)nfA$0~6Bz z2GfEp2)A1%pf59+dnRIfQjiYNFx~ulh5Qch(DU5>Y4hgi+B5ktPOYBieC6);c7zen{D9w5 zbz8+P+dh+}W2utee0asX(zTy;i-^8Wj$HfsTkIT#%@}!*#7<3Ry*JRFI!*i$x3fAGiBV1uFTG&_M+Nl-K8^rbw|iRwDf<*uQ`PO@OWtlhYy43RHU zi+MSC7xu}c^=VP5D#>karM-FEe1&BjemBdiCI2~%Nd2Z-x$D36V!zMSEECu%Wt#ze zT>!bte53k`tNqZf*?CvnK8Od&p#%HlsHlLdEy5NYKuvN|7-S%kZ@%cpvxjiw6*^IP zhF$o`Ym5W*2+vl6_z`WyQj_egphJ=PUemSegZOyhm2ZJd5FFu}xdo(mO2w_I~ zDQqQBq!;N?>m`Ih_qLcHUKJ%D%QLCDeEY%SHVw_}K+*As>kZC=z7*CCCf1 z$P`Gf-=^6H4_8A}5Xbd)_edFdPD(+ETo(@O*?~a$rugB_-#L7r+Y5ir8@TsMVcaWV z6V{X^{DYa_61@g@s@|mNiXSqJ9+j!m(G@3}YV1%49@aKb6={ctWgFDr*=TwP>6Sj) zcy$bwQfwcO1&#)OrEdD%973qcf(s6qb488f^ej)`(<<~A*E%S%Wx3jIdOJS!en<-t zvVqN4N;L}pI1^{pTzRm;!rOZ9u`h!dQfmmHkwjt*8kLke;5@O+_d)fYTyGGVolJKU z3%H@7QjlL+ISA-mw=~@XJmEiFr-1iUUQUHguoG(=wl12Mbrd{U^hyS}?fZqE;r!Q> zVSkzORkkf>;LdOj_G@!(8B+|7^(|>sY^X2HypP}ow)YoH)n(s4ynEM|MEvLmQ=+o8 zM;02&dzLSf1YfZXUjY-| z7aJvNrw`inQRYj-^8>hjW(ojuE7X}7iE@GLCa{reQ-vX=YldV+lM1S}%VX^@NB-;| zVhV;iGFu4t(0Rg1U%REPLL(bn@j$Hy=aV+=Kg}P#kG=THji*7#XWK6Ap@zQQ>a$fX zcKFXvS3H%T>m|5x@S<;ScZzMc+cCFj7-Cn4i7W`sLmGiCsy$NL>K}&+N#AfzKgX|9 zx8duuBi5A2$V$xnKkMBO91w3rX#1d_@TJ%+Ie+>ReDFM3=^y8e5?y%Hv+nZPR5Puf z*ASRe^=W*V+BjJ_!r;SxU996jDfaJrwl1nTCBsxbyLE6Vbik3uY;)@xm32#Z*dpa( z8hKGmUB(NW|J9-Ubqjqz{Y%(|Ib~t7WU3PhfR1F**;JzW+`+aS+N`y1B*u6N6&;GemWj&o0cw*{1E^{*qRLIQ%M`{v+ff2Ql@?_rxqF7DaS* zo@GVsL53jw9QcFGa-n~2T_C%!99wWd2QO7u#9pQC<&ME?6V$*E%}OaDaSARKV)tbf z`PRCs}IXw2HlK3X`1em%!5~&Q3=Oh zxSCOAy+M@gzDdd#EuPo%@?rH?V2D&z;Z{(Gx9lyvQOG=K=tVjcum6>xwf+KEi~ZRo zOmoe(U$&wNyt_kfX`kxeuV_(CO6pYL?Ad2)+#YcZ|AHh{*+W7D+sDLvwK$~%6R<09 zDRkeE971(8Is5XZE=WahGLaT>Sp4op$dxXC5WgCwvyA&T{_;Y>GU06Z1RFSkU7Ppw ze+n$X_ID2kmpTmffy%Ts=nzGMS($>pK>nHV>J|+iceX%iAFM9lH%S#@Y7bprXq@se zh3r2<`n!MW_8#HU?FkcxLwKI`0ij_Hn*rF71Ae-XI`&PDC6gscvX%CEZ?JWaP7-}+ zXlYrmrF}vB6r;5UxqhS(O>iMZd&Q!2e~qU8#eaQOgm9}aWX?0fYDLuf1Xm6O%5#AQ z(ROssGiPPR5lMak2g`iUYVCuVpcn|T*EFq|XPiggF%H~R|E#fM%7i(xSq$i(Teaz7 zCXZn%`h@lIafj+kIa4GXjt&pVn==j(1Fq0?%`>Th_3tH>|UAb25+Ud*xq^TR4CZ7El$DXie7FrhTFJFlwPB zMUB~g`L2t7Rjm2aba{cB@Ups;A!u0s#r|$acYI&IuE`{M1-6Zddsm?Bg)0y{91V4; z@*1b19rp!)wqv?`VYLzJ{w7vhjwD8-duRrrqUt0|laXex(X09QK&(IPAlXC)q!c`b z3V6I2T6Z!s3p!yBeMXO*4$_Kq0dZWMbJo={J7Y}0#3Iw zv3-~=ReN&*W9h2n1t&&=>sf)okqrlp9$dJR$@jizugW?XZHb$sav$`)R`ID*7hBi1kV&zbE?+OeUIV zH>G5@9}t%EEr8(%SFD!b|C1b|M|C>ek>}aH^VbHTOfJlmpuro0k;Z*<5_<7-yGij} z!~NlD4kh;3;pJv{GQ1Vgg$t!=OTg_FseV=J)O?QK6B&~uY+*^)?kZ!_BH<^=1mtM= z5K_1m7#MAyHQouEOMDj;J^G3kq96_>BwAb?$bw|?lPWZk)cS>L+kk5XCos4Gf(O;g zM{P)dwlws2ZTK(ubD#Qo7`)|TyS%0j9j3ux@MQ0kIqx6Q-|tH7^E*8-|7 z7Q!NiWS`$Wya|+K1J_K~PaD*kz>|*$H7!^9Rtq`{3%;xU9_t&4{<>p23KmL{sYvf_ zt4x$x=n|Ajv$8x$(|~fbYpW4oIY5Nt)NWGqQyR+RZ0v>DRx~ zv|mac+cQBytzs?|7_t*tOqAlx0SwHdPVrOYh`FcRr`48r3isI-ceUi-w+^iPjJ=~_ zYWq$GN&4Z=F4HetM5zfx!le)y_hp<{7S}0KNfO@!j{b^OT zUhKE#_beU{Qt)DmFDqosQCab4KOAq3lK@iZ~@raIPC8S2|-L zZMl6x@Gq%=_scSv5A5H8tDZy;5sbkRP;oH_?{To13WXCmxjS$SFN`EX`2v6 z3*cVK+vod)iML;c>(Kl4R|l*S-``GSVjS{3)eR>_HOO8Sxbi-kWH1Civ{Zq#4l=fA zVBq%kW{829Extzf(`o*Z*(J_-85S7MX}FtgdAeX-47n56vg zRx809$Z#&7K@)2pT2ukz?raAw&cjbp^qO*|gnXA_C~MmK*t`oYzfi?HQ%H#O=NCpI z82z0GXG{Hn0HT$rUTP{Tm>lrnK4GKSBcjn-4I}HT=aUoGchh5vda=~hyMM2H^L;Ja zNq#asw1Qc96~#J|0&cqA(nY|V9Zn2&VzbF7p|Amz@?R4n)e#OXwQ1yAj8C*TC5e1q~(2^ma5ypU$hBfrW_s=4X_R6G(f96jA;D9)g`6MMl2p$Lc7J(EPzvnqkpKQHq&qCl3(n?hWg%mXG zD=mF!_i^77!!6WGfac!7mOjhm6N1-gAs-T&(4Z?f4=?7obH8`Kk$olfb-AV=D}Ut> z)A@?5@_Zv@V&Fe)?wdG4RR~f(Rdra;BXs^oBkfLvEV&hWyj~y$s^^kZeW_y&syx~Vw&uery6sZ%ep!JX**6xcG| zg{_l-A?9PuUMsBqj=tr$(I<{g8%`bwlA=kM^fUrQ^?@9QlEZInMKxKer?z>0ZBb5V z7<)ly`nbbVx2bDH^SyrmI*$K-TtyFtN6rR|R2V(m{Ts#G2p;c2OwOMgS|?Y$oJKxC zkWB?5(JLt{lQ1%lgBfKFMwZna^@e((Hr~w6!h~1qsek*yNDg0n8z_ra zLyCyQkTdWkPiO{08@_`uYzB?PS?Dk<3yRyYtEThR8Mg&65!;BoM z$GMyE9FJjfCJX*g`dnAE(1uabF4EmVBi(<+AI?E%Wz_G&( z@E$bZ=XWS^n&%+hfJ)P-r!@ir@77H$X(&+@d5{JROBuF$L)$T}>Y2Ob*PXu^uMJN0 zvzBwFAptz#2|!rsyRAkXkY-+fP=2Ku)V~v`C?cx;l*<%^jtH#EaiU$b(egFS$^o!c z3)Cjwxi=$*Hkvt1bANIu58_z=LNhMznwp-j(r^FYpZs6n;G)QV^BpGahQeOl`_5q# zx>c6fqzyli$uk0ORQvkcSMSxYE~Y@+fsvD&2YE$B!{Bi6%VNX+gn- z+XMgHpiVnLruBpjLrRchs7%oOK8Jgj?imeTwa+Ba2AUmceJ94TAUM1kgu8DpPeE_( z2^)so792QHl^P+}3p|W0?Q71D3+4oz1;H^FxfV}$q`vP8{deH`3u68AyQu z>e7eHfPqb^f;uDFHSL3xky*?62@Qt;()l=iGi}HD+XQ&2%BFJ*kBF#>rRZRMxI52K z52dJ$rGeXTfaAL@nz^DG*_3x7ugdhAAt8|!a}tz=#3CX}!xmlPDIHU2J=Kb~?Htj3 zVlU+_#}AeqDC@;CTOQ7i= zX7iu_{C~cPJMbL?Z|LSxfb${HhB3_Gbj?~v>O^^%4m_>$D?Egc2Ya~QJG%<^HXS;x z#)(R;+${iWGQ+gJc`taDrk2)+Ik3I_k*^xlvh4Ke5aOsMuK z&)F_WwX%Mb4^ks#1H>P0JDWMhCv@N2A1%Z1I_CfJQsAy%$1(37n9N)@X37eKfW2qY zLm=1dk&oh;QLt?6uPRyrHm90h;4Pz{$|p+!Ia(UbA3ZJ9@b(z~ly5djGySrrhsHX) zsxRcfGX!+!Sx;C^{lc+c`44K0M}bY2IoIZTQXT8 zl%Hu*al6`~S_ zHC8Vfoa;XIlI=N4(Wga%pz4DC{medDh|Vy!>k_`MFW@P~PTB%El-&5(ya8{HCm%Tf`;Reo!f+DENYSMD6_Uc8>T}cw zaOAvS0d&2)`!@0O2<7ExW)Q=m?hl5~vTpU~eC&Q5imaN;$;tgu0M66{ca@ zup}A!UoZ6Ek4=<%p<$Q3hL#JJ{BUkQiN8kje??WRqw$kuloj{Rmr1i|lvWG_<`+^e zx`RW~fwXw-T#ANTy(EN{RW%Cj{2=*TGS7B~nsc(hE%AGJXY74;b|743wHZx2kaGrj;exIR zn4G16UQ<8m@;K$PJIj3I`}vJ^gPJwW8{-|pt6(b8@L>J>a4H#|GLL|T>V{O#Ih2Zxw+fB&~u#KR0Dx^Xknu`GFu^(#tl%^*8P6Mb}}_~4xr=GABPp1 z>k>xkmZqw33t%S^9~4)1n#t^m0jj*EIjl5^z1_?dBE^KnYzp15b45Y16s7qX-H$9B zp1b#+tVMR(FhN+U_V~j^0Dz=c!Y!Dbtj<8`YeE!tyLn^j7G5}Gs?o4b{C52B{O^B8 zl7AvhrVUIYR(R@|za+WUey4Xi{SBhs+(!s6*^Lcf|4wnulA@w-yQ3+H%pmh#ad28a zG4Mz*(K8=f2(gOE(EAjmc#8(>wAlJL0kC-@ver4pmXAV)1A9}{42yiidQLPr#&ukMQ z-KF$qeVrz5uT*`4;Sa`oBBm=k-JIZi^3VmiJrLrcqTqcupHUl)Zk2JS)q=>yo+G`= zSIrRXYBj0sdyIvTb%hUxMyQ$BT@W3v_Su%@l*0IL$XCHtccR@>JeF&l$7M@0lIgjFic4u0(aTv0WEyx(Wd<+(<{UA zTz03jXhIn>tQ2W#4RZk=?MFTD=l$(wXAR`HAQ6@uc+PszLj-!^$D(lN(qsfN2+m)T zqeKS&Jjz!MS4&b2w9MD>!#CCPHSea&n0r%udV~d=2&LOKq<}p`$IS(FnbbXWU#$NN{6$cK7!yAjp84WLcLX*#aQXMW zFJb?+-r$&vZSVIB`HjKD!(Df!j#>MVIzK_p8%9jci(frXdK)NalFdFB=jRAu)|FwZ z4BR$U$TweWl%=0f`mvCkh5nl-HCPYkwDtVJYcD@yJS&3b6p=p{!WLS>(`~#|u8rg` z6w<1366JH5>25?vnqVHX6x6qt&S#fkziUPz9U9s>z`GR>`4-DpxJ>M<2D5{CE4+BH z@k1%zWWZ8qv*?lZJmkmLJT*CIq`sY~{^~C_1`e)#8jlfXh#X;uY5$C0&`IG8H?g-R@)7qIC`sCfwi~CJDH|7<^h`Ts=TZ?_%v!+LF z7mMKZvNJ^DY|`!ja3p`(C3p=>y58dswf1Kg?5t8vhZhmOZ_K}V2tgM}q&k8=0sb2& z&$}I)?NEum&3s%{eVtJW!iajB@}C$fuqTZypd|gWy7? zK%gd@&?kW?LJkBLJbi8_v(aE2* zw9+Do2z!E27qusnSaZzGL*U~rL?^{e7#XRs$L>Av-uK@aj6F8H2lTG0wdR^@)~YIHB2}aa zhnz$PMq_+p2u*1$^NT(`!%DxM6U2D(9d80SxoxGIJIx_SMZ@S26rm%GzH$;!U z!MPIVHo2`|y;d%VWi>QCmqqix_&I#@(>6tjd`O-LLuH?Y{Thb~wBgL?^R&W1BWn?_ z*%^1@6TgqOwB7OEuw?`H4PYT5vpXn4@i?eQhJ%~BprnK5y&I%2kP6!%=ergwaJj2j zh9>mviJ^21DdW5LiZ|eyC45~JzJGbp}=9R+rU*N+r!bIE` zo%G!=Sn+|p;XYi%*CC6@!9`rP#NRd$N8e%ID5h_rC7FHrFoZTNH0FcXdm+&lzsgKoKfFo5Lp2h z*#BC5LQdkF$Kv&-E7G9l?3nBZ?`&kUaHC*fWg|9Zmw0XZMfqhKHcYzbu!kY8Glahr z9Sx+C?=+6u@7UIBz=c3`2{4(D&u`p|mZ@v}@lNh&9;ItqhK#m5l|>qb z8jLrqPWiPi9Uc?hA%|799Q`3hl6X8J=nOJ0{OhNkWTrqH_5dFM@yWD0%Ex(%4OF?( z@%C{el+L*lF+Ftj;$@Q=k!(a3nRb;j& z8@vb>t(o>^5hz_7T1ST9xcMcSo*_^ddA_kLL@Et`p4#zr?X>gfI>Ssv)R%JFKuU!# zKi$wgC)yfT?Kb8apl2&;G@4cG#t+=Bp6^!RpTLCjf~|KPUJd7x=wwfvv@)p(HgoEMD5`a5?zS ze~fy>zMEp%cBeuHR)V`BijrH2;_miqpc;DGYeU-ucEkv&*Cjxmwqz6N7JU^eJZ3%3 z)su}MwZ2NVD7HetpI&&$Y0m5hFMx~y23(mz&g6mTm;zFShqTZyb$j@j#J6TV68lT4 zMe)cuG$wVh?m~3{SzfMajl$#0j|V~_k~2ntsL0vcTH;tftllK=({+Ots7m~PxZND% z=knSz2~6ll{gU9BshBb{s6d4uJ3`{u{n7j(b= zIFH1BQkxBASuArtoih!9G!ZN8oJKi2WrO8A|F05ZW7~@wEQ|{!&87b z@G=mO(M)o?cHJ}+HRUuwYAHAIon-KVb>;}tZU(E7UihgET&;Q32;H*PH1Sh zA39hszg-YE<2v&`7_~MYI|k&|&aL05Zy=uNh*OPVCwV->q=6X*h-3sn(sy==$b06tpy}@ z?0RjU5cD+d#Dy^V(0*HW8xDCmk!s|7`x)QmOc=}R$>nA?;qe*Us+}WTc$c&X<&|ZQ zTDwiorkWb|!eNbZGg)Em6Y&x-;_5Wdgt&We9PbSnB?Z~;=ZCm)O4|fM1X6%G=3GC{ z&V{R<(Dh<$j`u2U(slMsRS7!u;A6tVTY`g%SBx4#r`nP)n!gn?M86Ylb+~k%Cs<&v zeP`qFw|o4Hg5n?O1K1E)$mEQli}IhwXwclcxPu36B>@Z#lLU%z?&e$&{UN~6?*!P7 z>TRxD-tGWj!mLB^>g@Y6tyEc>O&dSFfU~7hpqUsZaCB^=K1#B=XVSMK32zj2 zXO`i)&l;Z?*IZ3t3bSGk?k!eZzpARitc$*E#?W=$M){FRXx;|H1U1_M3ue>W&qnz} zAqyD{^l$e>yIp?lhR`*&x!)Q9wxh&RQ~mXrp%C-9dTY_bJJ;)TU2gsRBiQpjxM_*p z5orlMS0^$da1Iv^wUJ`)I#z2Om-b;Xb2D4rUM}g8w5$(ejKi9)M(o0x zhM8&1gWBa)l#^IY<00)&eH-14H^-KKdS@Y#ew#8Rl_x)qO&{0Yk@UKPIU4hv-t8nx z$qe-hf|!u+r%rV2{eUz|#wqbeS&D$kyEwTJb>=!|eQ#ZIT#_J^;Cf=z5Mta2T)8sh z6ka>W@YyF{PUGva|A;QSXf{2DnIuP_`D7ui8;PI&;#Fzv+|mONnWhO+L&MI`&%?hy z6zmP8Z_*2k6ik@vMy0mBPJ#L62>vkDc;j`pY9Nv>&#+o)?r>Zy!9d;Nu?HUu0MTgd zO3;CU2kZG~WH@LsyQg3{PN(&6-NsZW+77FxL1h!5btE4x7iXF}#a7BPoV|7(5s3)n zhzJ=V{Lj2<>*2*D`^yws@V=4InzLIS*Q|p|^{8oQv{!0Ig7a7P(^WhJJme2QK2b-?4}PQQMfLK|7TA^7E7LDsmfUYy`z7?3W)Lj(_%vq6IKF2#?uOf(nD9*F;w=s$T1DDR2&851_0(X}eGNBclL#R2zP z#=$yOR-n-pfYj1Hy*oIlEHj!5K!slS7WS z(~Pr+)%{3!`pL&WQQyqzXH)GJHq;k5GBl-X6u)=v^o*B8gf z>bRAcq?RvJ2@U8CHx+Fw4n~VyiE=MT+YuzD@XWo=k=-3Jsu1JW+7>a%;W2Li6DdBE{`zR*2^4mp^IgH4C| zWsyp>)RkD=E)(}x#(Z-tF{OZ^AT5%lR&-*d(~!;8qm*xiHIWT#k$mjQ{V5i2EHie8 zl=!%;^-QxU$L-g>Y0G<%*35+kiM>scoRA}}bf&w&CJ(EGSsD#51vC}y-+x*b4!k8s zhQTKJ3^w`E$qzy-TLJuDaM~fpIMg2`R+q=QaB>uGkd$-|pp)#+Qcs%+a{oE0@UI35 zx>rMtot(1XqKg$*yFuhJ3-PuGszw>5Hrl@_m<8n z$I0Q*^;_u;s6u+aDkEKrP|#MwR}QJd-1YNQ-(E!L-qoC@%g?Sxr8mbQkr%~dJxhS+ zNbO59Tv8;#;j=%B9LZDW?6g|+m299k>g~jgfQRV;wQJ&o6CF_hOcM?JtgWf3oN@C$ zv=y?^@JK+D(-_4#UWQQ!`U4}(XSbJyXj}4R5M9bvfw+Wjg7bF(O3&_BN%xseI9!1C ztnAn9TK{t|fCIw}b-JmCWl+uF-RM^%hphim2ll&?=LT9pO+0Z2$5d=1mTQ(TwoB(w zF3>QzK~f_r^YG`)!js?+e}BBn^&gnjYiES*_Uk+Z_R~+(ZdMvNT(iEPcN^&))t5i0 z6x4AE+o-Uu`f~NTS2+UF>3E6y3L?@?#^ZHV?##{clbHu;>DAAh&Cv!LsI2^-@VDRb zwED%7FL|o7uyFkBf|i!dvXU9kyT4g&SaV@vX=>V6eB8Y02d(u%#-UL~j-iV>I*=g* zzBkYZRaf~Q!Lk!bQ-9@vRFP>!-VINsJGt1vl%k**$Rv}9?&N<5TBKVPEauMOabq|kjzF&gEF z#CHf%0);|DDdMW@Bu6<+_IEHNI1F#*q*~yUWZ1FVllxs394iqYmoGFM_hMIvar+|d zH?FzbT%3g=Iv%93Fj=9fD6Z2W?tS@BPkX%fw)iddXTD;?y)}^4i^0kCm`bSY{F(!% z+BQT>e-I)zcqDl4bQ_FaZ!K|oRz2MPwQN*cH(rxG3Pj`VBy>IYO*Of;p|-vh7hbM&*)804oRFpT{)5c8WFZa0gnkBs;?M2;if@!Xtvyy6WAr%{{_M zCn>7j#Ev$v$M0C6KTuL95;$4 z(mlentLfbRpm1a0a&?LVWV`|O8zV7D&w`4cGMc12h?9;KSV>FKTT#cFtw1t3H#}5H zc4d9K-Y;B%fj|W(jv@~XiK}k#i*I+zUPQ(w8#|d_-&5ADwJ&@#oJiKk;_U^|R9zgu z2YVy-qvt(nZQRvO;lc`EI9llEU-+wm;ZU?O~xtAbuY{n^y|niqmZf14xj zuMOcjs0EUa8;ggWNlY_y{qR7cAjIAY8|pwC7z#?q?9vh6bpU=ciw0!Pen$;$d(UT{ zmM`;S&Srw!jk4`<<f*CykE;EGy!N{S5kly)VgR}5*pA}iD++F|?>X{CY# zu1_2{bUJo=O?dIX9W7*NkhtAbViCGim!q<_$ZFzukFtf;G6w0bw!%C=nj}$6_VF>Z zSv!R~l8{St(Ri*U(%LS9WGx+d`<=Y;v~H`#VsndH@6ILZa~}w(cfPRO1`$ErSB@n+ zlu|jreHQlrsb4)zIw1kh32UMbU?e}5{20^DV(7)2tie>l3S2ogVpILIvgP+c@W=HB z#Cu%~*pvrOnVc?kY_nz@mX(phhd*+tB8XDj8Ts*@_eO@G(>K-Q;KB}uM0N|Nz|@DA zr#1EC0gj_CFV7TS&(p}MX)UKN3TQu%m{2?_KZJ7e0cassuJ6w;2-q*Ev7XP-+rk`V zm{|ICgto4BP`O&9_Jwqcgi{!UVP$n^hQpkP`vvB!dwriC&@jE#)$wmvoq3Yo&Cwkl z<-&2$1eKKFp%K|DU2EO|=Qyi&`~xTkkG_%fq6p|kLKd*(>No!U0`q)ru_6C*>&?@+ zUqDaWNKZnPcQ)&>q6Rd+cpCFR3bZFPGnKgO`;II}TA^vj9i& z5c?T1vD6M%s{ny~kG$b&R`qx<6Wle%AnZ0u7=2#v?p*J#RgFie$%$sm;lzQ<OlWC_c9t*P!Mn+w^Gp7He%ER+FsRRpFx#sW#bUL2v><5N~2wEk?vZ z68Lhf{96ac|~XmxMYsJ)yWK}o>E4z&v1rZ_3nTxpr6I=UyU z+Q6DhLO*q&I5LCh8w2;{v6^sg@r!bz+hiAYSSXtUda!d|aP7iks?j=Fs#Y7__Vx|Y z@TTK26vcCRv#_t6NOJSIqBf$(wf(zvdn?z9_FPAy3bHI(VS=Nn9sCF+Y~+oAnQsNdJ*u z{o6x&7uYTLbDQ40i|kbfX(E;&<*hAwr+SeUn^NaF*Y3be@cioPquD~y;NIL6ri^6H zl@L1=^6hQ1AvTs?n$WX;l)Suvw(B30G}om`3&K zTh>&RXVgXHv|Hby5gZ5}XPrry)rSc!!@G)^Yc z=|0DjHPy`B_}Thdbna=+!*SE#cJ<~Bd*zz%8)$+JK6DD&keq_|HAuuYG*;r-bg&bU z>N-o3TgbxFC+pF3!b62SwvuoRYUs8u)gr*`>Gbo zN4c1`Oi3Dmk|L+g9R{sr+G>+%?l*z&5bUx{Lm^D#;H;%l8#3PT>w7Wf531@=nogg! zn&mjCNx*X(#q&@A_>KFyR^iBca?SJ>$#1Z<^yw<@u8%@K(y$q!X)?9~%N@h?(6|a; z##gm|8&Jx8AoA{;t&WXoY2k+{@6`%Rkf%VWk)>;JI_)^NELvH;u1EhMOBcIA3kv~* zA^_S72QMkXV%EfD=Y=<}HO=1G*i+f~#{DFNkZ+eKYe7)N>!LLwA;#2nh!A*xrkz4R zX<6m&TqzC)KtR{>jL^Bimhj+NED^LSAAGyzCRs1A)ws{w^y5pghc37R5easMhmK|Egsv5;;GgX+_;kBxSf`%FGZ?BlQ|#yo^>rs1;w(>?UZJ_*>=8NQ7{%b)mgt3=I9w4PJifcSGjGr~iimBNX3#C1BTK5D zF|bm-GoG>>t?AyTBh5WXAZb1nQ~PZaL*;H;Qel68G6=Y(i$VL_y>i7q7Q*oi_m+ zjfz?ledG>!oon&IAQn}BU}1R_MvQl^qx{!#eEmRPi+E!o!legbg22b2UbYv@U?O2m z4ERo0v9i=*^Ksq*2b0L*%#EkukfB48btQiEiJlZFd7}e;&Gj>Gc}gx9&6%g55Ecqh z?d~iDsyz*MBAV@ZSudZgosvGvK5oBD^m)8~F~>co9X%=WO$Sz7u;J2HFVWqzQ&)lI z!cUK;D3hw!G@GsD?Q@VzMLO%Wel>eoswc(OaZ9P_vY8yq&|9eHpf(B;0ykUKh!+)! zxE5&<593+&@zrkHz5u(sF!%^NH(eX)aiYJplmI&Z?~l>`FSh%4XP7$sPTqBILn##P z-HRs;Wv4$Mdp(yse1p6YWOOZ70o6qzwd^_W+?*%Vc0KNZ7PAoR`-!|mlMWfEFVo{N z1phFI&1_-+oh4)wU4jpGFl>A-RafpYr51dGW$s>>3X20eqvJWyhx@ue+upWqx?f60 zbKm52wen~QkK=J3E5^^S8#b-(y@irCVF=f0)pRN$JucT6RZ7Z<6VRo&2rU1e)_9&i zD&5O?<8abkN#-$^6?ZmtWzG6_oO_sjSL;U{f9Fa$gF)N-$}NmzLRzQ2y^y!3Bp0`)~A&xN{WogNAzS-3%)H`G;|8*SJF$%qh^(ybPc@!w0GheA1; z12ZJhWXKN)E$^yr;;yAUdb%Yl8i(lAdnujft-i^5yK+-3_G*0jxWxUi5{*y)u6le0 zH+V^?goX?b+p&I^wtW4#lD`GrBCP8PYX#HfNAmyT-G8~ggPgBVR5RhAmw8DE*&?KJ zSACCf>it;Z_lU@hoj4#bLA13en-<}tGJkpNBAr1WE00Y~2EeZC^GSAO=IPUo0777x zMcUA?Q08M2lnH1L2dJ7}a%aJ^S&P?&Cshk?Ti*dpxga+hj4X^Ge)%oqKcs_o<=0 z5eOzJR!xkO^!*&BD%z(b;YdyF4YtxrDbbS>bUjH3xJ-Qp&F}237Jk+`g%o^GPo272 z8Y4his3Bh!Z$SlGV6!Zu&9$lP=?T?aMIec@I8pE3qsc@fneX4LF#(0Clvo@mtSvoKa!22feG~hW4YF3lRtd~8j;SA=D*TtcA z-I}BQ`~M|`{tZL?{Ivq(_T%{Fa5F6p1If19I81$101+25Hagw`d7_0MUD{2X%QWf{{kp@zSgW*mN41h@*Hl0m8zfbC<_SjbTMhk_~3- zJ$LE3T;$2PeDB%`nwnr)B`Zyzmv@xtrUXq@%<*PL4Id$#D@9wxOXzf&_47ol?U$FV zmvAvztVWT=&;|z`iBzKIrcO?9ff6QepesRek_?sBVxHOZ|1_CV3%Ng%G?vx^CX7E?P zh(`6i3*VHnQF-^tp|yZ^7^q*D|MZ}t%wl%-B^nmHsls)Gv;qR0q)XR>3e7PpG5kQd z4NZ}5s;1uw4{Pm9BF(*))pfm`b@Z-;7L5I;#dZd6KV@e)oR@H$Oc)i1i4nmb!TOQ1G1Rb7{<<0KA!x@k8v zBbAOv@G)1b1@*?(Y56kudkFV&boRiY zS*o%4+czS0b^0GTdUZ;e#EdlPJpx4a-^c)!f&7D3ljpWy%70uoqBvg{ZIcHt)PU@j zK?>NQ*0#o?nunBeedkerH_IA8JEcxF@PFzt2LWG0C0es6iD@`4EG-Y_qukw@@?pX! zEkCce)CJ-))60qu=xw~07-Ipf6D!c16(48CQZFJ(Trh_Vy_4DSZui+jR-?aCTBwAQ zl5JR8tL|CbS^n=F?sujx_Re=U5N`289vU%}?j1Tz zvaIAGlZd=3MYWTZ>U$YmXzS{ls6g%O<1i#_GvVy4=lPf+GwA5qfUsKErF!A5PTyct zEE^{n2yVu~fW|}`9$ey7yYQtit=uGsvxHjKHG}N5wXPbZ64BkY~xSRKBKo?k>kMLTS6zxO~7U86Z+ zmr|3JTX(8`xD4kg)LNZ)*`kO_cWR!y8gO!8n@h-S>7zc-u1<)}pigMf@&yFD2^n0mac+~9vPZ~pO)Y?>1~!uJz?OzXzD_{?RW04M z%h=teLrf|4E-Fgd(exsm@Co33rbab8DVDZFqhG{|{v$+1UJC1X#k7;j$PlDVYSsr* z;Cu}i$zq$P#eh@^h2_fBI z!sArnlijj|Gn*k@8*;p>ecUucN=w2?Q>=c>Xc}nC_K`_o{YoCq6Df7oO78I*QQ|S= z$@tTXsKshJR1x#f+;!?*n<+}(6f=R3UB;K3g%|~aWG*MO70z5v3AHpmwDlW8{xiKR`Vf606UU2l$H?j(a5;LQbpsjs#MmEsb)P<6-tW>orK|LX zUk&sjoLFWo^+*FLjE81!FF=ENY0yJh({!;YwYbM{!fMK9hWog`vPAHxRy|KJYj)Cv zYT4nXH4hQKygNQ}IdDeqB4t{-znkXOI;Xg8+tAR;jfxd~@M&C&WV|nNA1G~dMN+1p zHyq2-csgQ8kg)kT$~xeHAt2JMH7Gwfox7$qIhMym4!4Zd*}nT{#2^*aby%fu@|bC| z>uww-W#i$x7mj?|ZX{TWmSgPL5*JzGfO_pY{yC6$db{(gp(e^?5UiCcueED8UDf+= zKNn^PL|zAf-y)Z3;hug%LkQz_BC@%wuGI805^}q=fo3Xi93tX*$(a%o4bgF$B=ZV6 zjaahOPen8lx`aPGL%T2QKQpwhye^ZV<=-#d;!4HlnK$(u<1@+8n$Cq!>%idv(vB9U4q65Pk#zvQFC;SLuI&7Y@H$h0_oSjP^6Rp+(qSBimFm*Zo=}G)- zsH!TB;#MB}V3bJ8PPrvJ^8c_VWUVV9z4OuDKw~b@5 zSp51ugd{dAtI@MdR|r+<&8OKrDF#v3i!1e}vw~aNgPEgnrl*~EeLrnvsp>VF=AH28 zi4u3Sm}F#m1XbKl4w`!0unyM>5b6$VJ!e`3nN)iEcT!z{WBoh8@2P_eCy-4J2?vBV z&t43D{~MBi-PKlfwt8j_Val$>Dq zx~%BauF{J)pBB|3f*R-(C5ov}{J00^!+L`om93BZmi`s)mrX!w%{;xYZy2~5p;^&%P!iTE5ye%@u0#trcDOT7)3v3Q^6%u(7gGDfI+bHNtf4Bi5aJrP>w%B zzW)%lCrTs;x^~c|VrThhbkP#j?VFu5nk?Vm2^)mF=+#nUHbrOV+l)fMaf~S0{_qD; zE9ckC8L05aiEmgd#z07{b%Oa!Qf*;20Fi`?gtcIgfiAQa6XaiQD>&^h?W98htRq0^ zq1ITee#+=KAH9qfzRJ?+<~wE>d7?KgX8$fyLRq)vF`V>y%iCKH(Bi~$Y@JiW?L1od zX%e(f3)=t7UH25T=BRzyCE}P9G*pkW}dPR{TRahpZr3rDN0nhs|+tA zZ0;0XGkzJk?89KwnU)&ej2;0?(&IH=S{F^PLl(AcXm_D4!b$k+E+2qbv+18X#N+LLd z35j6Q`vC%(of?F5uek5)gg(Khpp=8VVCE-WVdbWyy}+#jH{vjl7UKXrurF9Ry!&4J zy~2HX!ec2nT8h;zq!kxy+o;S{&jHrbBYyf{B<2r(>3c$2Szu99%Eq^cvjjA}tUku# ztuoL`38(XFj-#0ieMRPpN}nfZVZrhuUVKi{^XDVk%a|ncR|IWych5`0xtXeR%_|JU z|9~;cFDG8*nry3=0|l&=n3{cS$}U1=R$!kRliM0Qr$EZJ1W;2$J+77*HzHb$EI{n- zC0V;S26%e0TxnjXmV-0p@1Pa2TejI5-IRV`wIK zD=)K)9u2|ea1rHYR@&Iw4@OCjXk?0y$;Q(YiI;*ZK z^k1#chhHAjT;L;R`GUacbWX(Gs7YbE)=f+ps>#;N1iyWMHsppF`K-M(nE{jFHLWG33jX3>lrRem}o# zW;5(MF_M?lqMXd4VPEs#bD*cruQNLDxt}u}JL$DsnGf4KpUjH&QoD=A8936!O3a%z zRVqBrZ`tP0zbk_H1vvi}hDnul1@Blvr;dk1g;^wvy%7fSkaitrLm@UZak5?AU%?2` zw;Yq+P7^l{hWH0^06i$}3@OkfYWQk<6u^Ij+*t3f`CjGK zj~$}<*-;aRWa~=#Dupwb=oMC%IaNZbBuQ7P+2|Axh-$UX=}92TTDDb)UTD<6@718u zCNKr!)7{}T#qPYF>97thUr?_G2x~~!{4?_HErUP!_sT0NVIZL1J$b=GfKLV?;Or+y z%k6q0nh}7^Ai5ZwRZ})C+^awy!`BHg5dm?@l8@+g)avlC_GY(w2c!UX?%4=hG36%g z=zV~o2iZ+{r=mjJ-bk|12+1o$A(hA;f(Ua;I7wow_aYuIOQBq%x)Ez+HEb3H_TfZ2 zWYjyXsjE5a`lmB(k=`BU4I9b7;u@2R$7;Dkdn>o1!OEU@HbLDI%=*b>*zD*rGst~o!~a?oq5_38#ea5DjZc9r+htsPGWu5QP8 z)zvn+5dI(0sLgwUF^iQDJbJF~4<2fb?)jP=*Z56dkDnQtO|mcPR|?1ud(}ic_o0(P z)_}x>^FQ)e;yZA4J++ScI;5v`0U}n-JTQRg-tOD$u zIE%9=m?w{!j>17(*2rnaBDZdyOtNS}n&~9g=nTx70%K1lX0K!mBs&=do62?5xNv5WF|{i#VtW97Ye z-Sc7Fv2rG2Ya<>YE;(bF8jAKP&xn!5vKanvW=|Xq2Yw&p=OmS@b7Ed8R(RYU7=DE0 zBfsuu3iyQ~2Qc$KG0!czFFUW6F^5rk)1sZjWVofdnp$F=0oYAFy@E^+&>--X3;ye|xf`qiy} z^Pap$9|ZJ;Dv)${GU;NSxgkS8`bw8Lb9@jm_Efu}FfcG;;{?G1q;C}`Ng-lMbWa_# z#c{5WMc7gW(Pk=rrPx8gIc>uMmpbyVw3B} zHY2Tfz?P9=*!UqZNQ873{qYkcsFw5>A^jJZ@C^W8WN9kGUH8~?x6PQVVlvFTxZi#? z`CkTr3Q~E8KDB4bBvST&%w~ey1BxMF?a$v$yg-o}n9lub0nfQ~ja9)}644WMIp#v&4-;um)>m@M>^}6Oa zTk7Ano&J^!evh=o2@pq7?#z;gB_P?7)A)~v3*ciuz6b=!hEny6{esySFnc@|dL-^0 zOQ2?yd_E?muSU2LYfjBT} zhe{A+xcao|^&g<-dqR%Pu%8o}`e7P!k=E7hiR=8lz}ZO0C`4?)L18wg>&twhfFNgA zum$ypXd;?h|B+=@9bT?=Mh>%|iu);<05Bb5rzmNL@u1|44YO>Nzu)iIotA1RRJeY> z+u*fl6M?fH6fMH9Y$gMeSuq<4kK1&2+MW{v*;Ev)ZH`5C>*nF`#GS{J@td$4KDbD6 z=kGb@Z{Qi~*_?`ihv6_o<1UYss(XY5`FaI&|BonfGzYc*l+NYder>}>3+;|}lV7K| z`(Tp>ZXefU;}SC%R|*`}b#VVVf6pHZ%ib;Mq~7nu*-IO=^UEpoqJ0ps&3^iT(7U-D zbi2Tx({>Z**bDKm>L2kw_#%P2tJLQahLvodx@SkVYUeV=ImI+xIg<|{yE96&n*P9} zbpEBNk=c5V-0nE}?e@^5$#8V$8`ADH>4~o>yvqMnzd>1cx*o}Ht+v6Dhf1?m`S1DO zCIqvtt{@adIOmD$j}e&KARiD&RINU6&f5Z)_&dMW`5i_5frjE(Fre>uY!qR|I*y*J z_aK%jb5r^R9F}r6eXklXu<(U58L(1tTmnj^E0W)xTD^!_rP>swvppK_XDld8J?%TD zIrTsg={f~)?h5^jU%$FpIe075Y56$!T*NCENcUkfoUttfy$|ukEI2Op6md@Ev1eAP zO-y};kDLadKmMPyC8^ZeM3s=;b=8HBKCKO&IovfnG-?`+z39|%`NowL*F#Yd5wjRp z2}EO_LdKREbPbYi@&z{;6ixdJ8#iZD)QRD)|AW8%$%AKMO*=cGkYaIIO|?qqK~h{S zP4-O#0`@-p!VTA&KG_4#rpq5@I6>k&wD@7c)3jq`txZyU%e|3?mB)>n@AR->K zlk_aWhyMX?{z3E2XU_RM!#Y17xoT1l#NQzhrdEs?8*#`t6jiO++p?Cc)|D%9sOvyJ zW>N9zZ5a-Pn`@n35scChRoZIO=N9FGZ2%uxQaFc1pb1j^;|TZa5sQDE)IZtNAH4wv zfr~jY;d@Q`10I)P@Evn20Eoh$tpIn6xEp-1Sa8!&rn%KRe#mON4FQ&`3|`Ik=POAP zqr(n&6;GLBnOa*u$*xk~QmA){F}vhyg*Yayi#3?J3w}P|RiYbMUXhSu$+h1)S|=Zz zNPm9iq_n7-y;^Pk@1^4lQpnp zBsR2(=#LhiU%kD-MOKc+eTI((z@PL_)?#C0O}~?rX~utbi_w~u2X7Vw7U4hqp_1~4 zy=noabc^rZhp*G9qu-mU^O^4TZB7Qsj7YpkVJg!q7al=kaA=|gU$~DWW&gH*k(!%! zA+na7f_9L`bU`=@FYcmXT#RE$vqV#JV;TRE=RRaDT{NhBSL)kYuIQ%s??br93BJa$ z>D6TXO}PARF~Z!CNv0=1+$ZOzL^kK0R9LKG=ds4|x+S*N=c*A()jjoD6sET)E|aZ< z9QSU!{+2|8Y$m)jC{pX`{}_f0#ZF~<*v|sxn2~s=2@9vqNlmP}uOpD(`Qshopq)dE zvVsEOm2+ie5Il{2I&}q+v)4IQdj^Pb_1ymUKT(Am452ST3a0Q&J)^qGS2eD=nkqsj znrK?rg}W)bhODgu6Nxfce?^Ppm-u+1=&5sgSSa9^oZP$sugF@#alD_9c`DjEILU1M zj87@@idrP@Vb0=aY9UR$2&{Fy5L;)id6mUN%*op~grJHxG#DJvZl~D4kHHV*JrHIG zZX?)Hb8z1czwIDr<97F6KHs5cH<*XD?H_|TWZp%#tUsPu_T8{=mEKGHUNSn?>-!J% zkXPY<1J$KF;Rps0amy2}7i-$Ld5wJXqPhnRqTUt-Wvf*NqFTvfyH{RwL1o5y(@r+h zOGZ*SX!Pcxs7rskdI=;ma&D9%qFZa}2sMZ0)}Y-}z}xKruQ*aZfQH}up4yITvBV=a zI%}&8z}vTp;#v&J z8|Zg8eJSVj8zzSR&z6&LpUM!Ql6@v;a{33x$8JNz_c`W;f)Rs)MN*?D+xj)B-qbt{ zHJCihhazF5vevc7#!O2TT2?P}95=fd{{dyh(mx)u?u$nj8C9-R;DX@)*+@`+_V8x* z;J|#kPF|UyYo>@7T*MJq60|A~5(@sOSXR5}8azrh~lS?c#Kn3Puhc# z8g8FO$LFU2l|+9+!~_HwUToFo`=hBJ?@<&+3!TS(?Jn{pjD)N_R4ANwTQ3Ri=C(?< z+tI9I!S3Ef1^B8f3=TL%BqqLhmTD0R+`g`Oi?wjEe>)^+G9(Tt2pgBPnrdn%n^tcV zgTAuICLLi|KF9$-%n{s(el*>W`!ydi7 zYwcp}k4iyKS5~Co<4wkSMg%4Gd)ZTV2`niKu|X(u{@rx%CCL(LfrC!!GSBv>1&wwc zZOyxacolr}DX%j;VbYa!w&^5xO~Zy$l!^1|!G~VanK8}c_Ood$Z_Wxj2*=9dFHDa} zf3%00;%A&aQ9K9a+x)!Z+OA`Hvu%~Nd%X`VN%jB8SRH&o+<0HLBrQH6LA*k{?LeK; zt5xN_bb~GG}4}TE}kxu2cDr{Cm!(9y~){M_Az?;ndrt<_dwn4L8X@>PY z_!{fsH3}B5oxTQB^UaLFvHgOR@jqu=Y!tJtqgKz?OuV1=!2cg#Zypcj+r9y3OC?l7 z2%)H~S(2T~maXjD7-i4CH+K0(8T*=bWZ(CFFUc}xjj^v|9SjD;Fub>K>3M!V@B9AK zhmU;Becjh}p2vBd$8lcSefx|;#1`dzhesBEo3pV%1Xxxj0m75KF=p0{9 zIMMuh?z7~gMPk!_jd47mUH{;#$jR3_fTOfd0FWrE6&RVF$iZq-sqDa#;Ee zr+pY%GlvC!-6QXUc70sU!uQ;xmrnDU@P&Puj!wTdNq)WtSZ0EqA@(DWp?l0mT#c~z zt=?jvCwn^A<&r+3cEAuKTyB&gu~7dZzMCYYC*m`Ej6`d))T9fUfu8A}lkYxeQjE;b(@{)B;3J2s1H{bsh7Wc(IxhWR z-01&}WXIXe;IqOLIUyk-b$$K&b#-;FVla<-H43JX&NcTZuJDflsT)S!*?sL)afd|S z#PcY%hBW`{nQk`Rgwihr({Xph_D#B|HGQA0(}1Tjhmc zXYPUuOD?X0ecoT}J6cKrN1>Y!$vTE@1w^>)OOMd+PVQMyh&`CS7`RAr-8)&-wB8FF zLW;~`mShd*!C6##BUI}%?O^Q<+P5#?z#}I}+n08SY_Y@NuvF(m09_d`1R|k9>4XP|Jk5Cb8kk!L}09F0>Y*iF4^qXowB{8X5Q)Ba>9^DF;rXqgCEp) zyt!7=1xTce7jU?RKnrv{5K%yIsvlk}nZ0oLUawnrL)a-iGohU0eXc(R%)h5J5uh#P z=tXmjibAy|`tQrp(b4tibL?5tf>(t0Fwg{| zuBmxDAtAw43py}3h{_f+H7&TY(o{mpxK?`yfwYrR3Tu@-OPtcdmkgyAy$;qM=qQ#< zOvx9gVcg=)1N6$~uV>|q{`y~)1%BES2dDvyxgN-2C2Aq_^Wa<@hx6wlDxRp(8c8kt z+Piy(1TgUNbWSs?Z|xe#obaHn#J);aS_>G@dC)_Ly>prNj_3(v$K+l6>22QrV1=Gf z>DA9RJlCb}Tz5MGrW4fSezZ@=Cs*1mEjkW%?7{9*rn=crYzqrF(~=9d7a0?rCFNfP z2!OkHyc5ZpmftM(r1ZB&VvG?+=y=8G4^)p-KFXyvk$hjj2WuGO1@uMe_62x6yo1@- zke1!aE!3PS;GuD5EObW_ao8+0%i84T&N*fffH?#7HB_Yp2|W(8R@uaBO&U_%l*vbs zm2u~^JaFd9I?&rt7tTpZsUG?y8o*)u>wTpOkckT}xo0z!OBA!O2HkJrk9{|wB5Y>+ zz>t5@2C_52tQan+(NUk!2CP?1!8dT<_ol>_y7SQF$x@-+3-g}g*NPrq7vmZ^_;7?i z>Uc#fUqFvXHj>WAjg#o(m$C5@Ja@dK&t?JKrZ07(7*|0!f+3io9$DqTxRc7~t>-Tb zIv6*o49xiV*iScu9)HS^y0LX2-f!Ht#c}1HC5vzaOW%D04PCA=?y-M9$QNj%2!w6N zZdaK92zCoHZ+JY|Eer4%K_GJ?Igb)l-v`iff0L#4?P{j@ zVNHd*PCF_u)(*-E>73g3F$?wgXq8mmdQ}s6SMN6HaxVS?+6rd=+s5z2A7MV#D%}{g z{%3`LZKHOf2*g*u1WaW4hWc=_ydW)GW+`k=?@^E5J&BQ*JPzU-t1^83`15dT_2?n& z;m#X`WD=AN*+tFU<@@6pT(_QO58y>S*O)d!PTt&3WO2|@E5v1}g!V{p(GGmQ2EH53 zLq!g{L+|6VA#A33%!kM~&eh(4EQp=<53EH5&JMwEbiz$r%EHEIU90fq0K5lzY&-rIxKqB8yc!H(nLfw3eE3FYckli7j3~aGBcljsQ zb#@izhnAVBo#Sem$K+SXJX`K9mDn?2d%vuX2QQrcN$E?MEcupF)|ufEYd-u{y?47d zvp5=qr|2L~hnbwFV6(`^YNt3&t@xyM3ao6BLlA{gE+RH06G93;pYuyVkTywTZlA&{ zpxtyIa1>3rZXBu}B{w$uWEW1<^i(wp31Cy+Ku!1O!JdufK5^okxRdDB=PHx4Ffhj4 z#p5yT!8RJ@!-4nM$umG0QQ>fN{jw)9FrWS0pMln(9Q=5>g-UFOT9=8=(f6TH{p^dc zr^R<)AGCxYuZ>Op1ckW8?rjvQDWSQjX522HIDf_>=TqQFX*0*k0r_S2FO;IgfBZ1z+IJ^Mfq(CX_`=&1h= z-0Z1c+WD}T(Y;)W?$7y9r0EmBkF)HRnZEmDvPzRlPxOiQ&5;S?I~gzQh>%c?l4uRN z)LYG#S^j&r)*7I(@zT8YS7N_Xp}OqMAsYW|FP^qO5rxGqAZU-{A>@3ck3 zRANUj`&*F3I?d~sXhmxDJdHC9w|=ImJ2=EUJ2oi$9=Dgk!zK4^4Qo47kBU^#@HVm& za|_hBjP0Od6)}#zIA%Uj68_f4E*Mq_O!#~^T`=w#?6!8)GW*yxEXXIaT<%*@PW0bS?Y$f=HcBWByH{BwJ1`;}!^extgvXK77D{9{9% z6QN|tEwf(u;s9t7yR%Cgl)Xd8ja6%T5lDH%Y?aoEzwy|1ygR3P-tDP$L34HKFYd`- zljd&T{aE`J45n7;(cLoSFdFDF(Cj6P@n? zV?I!!zcoWb;sJ%i_&*szSg(K|nwZ>1_MU7{Ne?C>eGh!^1_)bD3`Rz8sJQwVprz6j7*O^-RzPdGlL1wtEtLUU}jAl6p44mkJSfsA9U; z*&E|iHz;Vi4XTxmd=3^YYb=4bC^knMs969x<)j>-W8b_5%B)({d-;c(bN7Gx^5awZ zSmn!Xb8Cofo_NEy5X5Iv4B&N6Xtl>A#;P)UBYT;#R$Ut}eO0C*_vxzgs7QauG=_^_ zODW*5?{xW(R<=0@we+n6{YCJZI_7E!7B|K*PTrEF75(9n?=ThGlJy6Lyc4VjN^XJ& znhigy9tAaq7%tgNxNIsI=N4v9RPk^~7&IH-muRP;{Be-NHPyuWzhOv`rj!7P`8Q;q z$l?0@-kzrs{F;UmG|imM772RU*4S^C?6D{o&kvCO+*cp1yOd10R80A9IP@PKV9bUK z_4->Q$Z1D|W$Aeax@D_9BBx4D*QX392qlug`(Kjl`ia#29r*enB#n~yxJG$>LAbY# zb*}y~3E&xou<3bzBqo|Cx**5U#wfxpu~>R_JnAe>0PX%m&Pxj?KaRBwF=|ZwD$RV{ zlKaWwID`IXf0!D*eoCclQgDqBN|ze~$|T4pwK%Tq$9$hS z`Xrb$ppijpO7s`DKTjg)Wz3nptjh^oe$t>PN zHbS{Qy;oPvn9?LD^3KTZ zd(elp)VCKNg)$=FRoQoIaDt271)8RKTek2Xhzg((|L+1g&6ZfuD~+<~O*$p{-A4sk zmVPoz2;kNR;0m~6IpiV)3B{6{e!cKmj^~E?70ais6HZR+yFMal!WL_#^~B8~9WY>G zQ3>!lg-cUG51*TI(ylauO2${)raB&KnsyXNSH24Ld%?LmLjC&>tE;rK>4#WOh6!zV z-0AHMCQ7xR88~C8J&Hq_|bN(L0Kxiy7HEnn{XdGE&n)xyiU?w`Jnl`Yt2?D&?Bptw(}I=tl=*A8I<6 zWlX*vg|%(=f%SDx7-n(5ek&d*1wqRl&P-tq%g*wt?1&^DjU+Lo1+snpn$;{SNDzE$ zLpQH+rhAL_rvY%mhGKl@j=zTb!M5dDgXdRnh$tuQk$xKp70fh#B3JDZT$EPsiqak} zGYLZJ6A^?@cqD;UA4Tr_lLgWNm8mk5!bYRmt>~QEHd%lM-`*KigBODu(qFQ8PKy-K ztW*MJ?Q$<2PdT;-V-zOhs%~D_7X77D=Jz%O8s|GC+A1o~&5Jz2$7QS3qae4m&xZe%-?TTKv_h8c&wj0q`iq&8?8BeoZd{?(IA0jvbtQU;(+={4%q@N$YA{(M zkFGTf@^@l#lWjX&1)ATD2>45a=GY@J4Z_L0)J+OlPn zf}?(sfj0$-%ODtY9TUJXV(4ok-6Pq-nCxKt^)lh4?kx@-q|sp3Oy`S@kE)7_AH)xk z(dxRoy6!E&Z&tjHR2^@P)D|v}g|6XO8-vD!M<=(dMaeq;Xo5r&{5oPFMY(sQm$aMi zolNm5U1C^{YO^L(M1)8v*@+#CcKz6}dVS@o@(>%q;{01hEk7d!`yR@xYZ>}+f**BSSPBM+GsqWy^` zucmk(?g3~Bz(hb)KOZD$Xj(uGC{gxTL=EZ`h~)MpU8fpsyXNh@4N^at{K1te%F<8d z+DWl+srcqwfV_e;ONe^6ny)RM2L^M^8BDqp`Wi^WMO9V8+T3PH*54wvt{{aQVb>Ou}&z7CRIJkLENAZg{#L zw*j<#I=C(VE{YE?JPNo}T~Ij^5vi@-FK=a)S7_9v4v7VEcvf*?{oBa2Q|b!GwTG^wLSn62t}3&~V-2@(yZPNW3(guP zcH4U?kT#9;%p73RgX&s#pOYpqJV3xVs_&3?$=BdC2aX4k{I%KF+0y(;N)$;)%d{2y zBO)bjNT|slTP}af&wr*}YkqE*y{0tlIZIS2+B2Kh zJmH1Yb*jhvT>|AUCjghox?Am@c~Fjeh=Jo$=|pzz+Rn%|1r`72w{L^JQ9;L(s^aD1?vN<7cPU(p(w1z9LBTkaT zvzv@zjnLLuo4IRqy8>#rXYpm>Um_iFnG78YccN*Io3wNe$KUyej)NQU^$t6Fj&Kv& z%K}4TJ}y1NuV{0SN4z~J#g{)^d={%6--VbyelfXQ$UR|NpM7XD*_iUkakFfDS7W2H zBIK{uo~XiU5s(%MJb94z^D-dc6{k~zN@7F0;cyT)BmYlMuve=KWh`3%`(;9q{!EB~ z_jW=rPUS`?BTu5Red_mQ5dr$)LC;P)zH%+7i%pxSXf;3_VSxbg#P`xj>MW}SGXa~H z@!3+_@uu;9dHYEpVm-ulaC{`y+1#COXHWP$I8AEJ=#VkR@4hq8_c{PbU=Sesa9d~I zJXqM`^x;_w6`!n{5D~cV`?2xOJj_&g>ekX+Omg$ffd!}Cf5_&?j=>ea_Zr}Pb#W&f z^S+%?lbj&9%c6D81Pcd+=tDaZvfr8KOp-CC(;67!V$+~h0TQy>mjM{X;J(eae60fH z%;$jO^Ly*4*u&-+cB1pOZ8XK9A7#>@7YtoKr~xh{^re%h-@_*WS118nle>+3z}}SO zDZwGpR3qq@Iqn|l5Gg4E2q8O}@5P@Tf+E^JVfm`;mhl>$Cbn=cZ#jzLT3@SvoPY!I z??A6WLa<(rvbM+921d*RHMO*7Xkb>uzKY}M>{w9tJLc_NB}^H(D-Y(c)4^eAIr zwmFe@#jDk;7#)9ynLSrMsl}_?e%@?k*^b6Ag;?*6CoOh{Xq9~9)>$fPJRr);FLEow zK)kn?&7|T0J`v$`);j#NBH&g)qI@gzJd@4tX<*2rCRbc82YLt3UcUTP5Ls8pREItG z3%Q}#*-6r-9DkJ${GiKKczya4*t}Om z3wgVkF0gva{2zB=>xJc{%zWcm@iHU{#A!WA{blx<@-*%6B2bh(K5jIXgYq8jhE|vj zFH+9$sbO=v@ElY3o$Y+J-MnmNcFH!3;};q%ByV|_tG5_624)Lhk4YJBb6vlqm9_mN zfwC@6#Ny#~+gMm{D5V)NROW6=?TlEDO2S_YpeH9N zn28N8GR*wU3Y3B5On{$x1)qgYXu|Hyh)%vBa<`N3ayB~4?UjT~8j5L!=>X}nuLdcY zVauaHxQtZ~oC(q`#ry%<>Aj6uZACjr?Jhu;*h}FInhxcqZt|CiT_Z2C|H$oJ&8=LS zufcn`Xyx9uMU?ZkYO4W#{CF>x`Mtr0;bxGmT4#q?ya=sqPDz48q@mPGdF{eqg!&ar zUjlDlYv$^4_VLN+iu=&>9M!hK*ERO;Li|@YJGo(qTXJ*Qw&KtPnOh{p%BiXw_3{DE z8xAiZkUH_*pBm-Xh~oNNsex!9Evz&F{{SBIDFY9%)kj|3L4RbKsynh3G#-gd{$+*) zx_(7|&2nPT8`XRspdIzf?E{F}KBQ%T z;ttDK?icaPkaWRPvCQe9k_Ug0Auf%ljmnYJa8PhV`9n3#u#x;m?x_|`wysi}wFL6Zr@ErwVbcm@vTId58(b^Dw(vo3%iDFm6*;i+YdO}_Pv$C7?`qc?tCFk8Se%_3QH*!HrinA8HF*_AV)UJhd7 z|5P|4(_q+u{U^zUR^Uu1cvfP`5CUm|A-K=)A98g|Gn%5|bQUNO5xzBXH0`Um7(%M1 zH7!@tpfi;hDj%oWgTp7VS%Sdy7RH9r26uZw!%XTBn|MnPwXeW}VCeYm zD#(vgRG(4*tuW_PS&T=fnFy_-jKpM>%HjN-;`P-ezWO+2DzRd2>y8>!^2T2YM6|}QB0pGLzu)*DiibC5lO6ZEbX+xnWTP@~EN2>+tLN?cSre&1EkSj>)g_AO7bAlo5{Y zCI))%yqMk`&C}^9$}f5QVj}SG03n^0LTq#8F%k7cQ{-jY5i3R=ZSC%0YEIK_WLLlR zJKMRkL9gt3sQmF3Htnb8cvi^O^m%}YGQEIsWzUM1An zmR)4ua)t&P>isH(q;;rkUmuZdPYP_fgQikSybQG03{d*o(;?HSc(JgC2X?NXKZ6|M zx_`Fk*}_{~>3jaqdffPVwl&>xi=2Y|eACf#Gk&w~7|Yr()!u85N+vMvK)&8ei$R<; zylN0JAr^f)Q@dhRcP!U(k+7~!Rw#y?qn-IA~WE+_Ok)9mHmeo7N>np;@@Bq_G_iY`T1!HWrYlMfWqSbIPO@!Aql zcgpQ6bZ_ai?y8eu@6$AT!G3~6is%74$YLh%;^YVWx`Sr=VCSB;*{r76_U+-}gY6NZ zIPs5?kXIeVIoD)1t)01f89DgUm!8v2IlnI;fF(G+D|}O9MUXL`1L7t6Zhd(vZ8F!E zXKK;#tKk1_Jp$V6XVOwN2)}={1qOh;pWDfU__hl00F_bwnTpZMv>NiBmv8WY<-|%v zu!o=I$pu42hqC%_5F@`=csY$y2Q~0IJAF?P(^>3=F_V*739%X^bZXxpD03bwR;zM| zG!h_STN3C!`>U1V(ziu&9-yCOB8^`N?qFm-vtneYj4yr!AE8An)v7;s%eI@fUkh9A zilW-?7nU?*Hx+cB^wKM{4!zvTa0{rI72M?Fei8?iXSkGHzfPl|2#JIcWeq{2oQ)0( z!jZX-*MFyJOKbnw!{PRnxnYyySOA_P(eTtzgC)bwltFZKN%A(yQu^{v@6xf+YvtO+ zKREvk^!A);@9FsmHd2!ViYri@_wSo7bjR|){6gFXhZl?&ssWuF`^R#=K3sPj+&Na@ z|I}v)_)EiydFp<0Sin$Wh-G(S{i@S~#C0-Rq<{(_SFBnU5Gzjr#QJpLO6vNxq6!_J z^4E3*4KZir`>j50!bQAvJ@1d35_I;LmHj%XBA+Z|l~+5(+Ke<9DW@wuUJO-wWLhr^ zUGxN&r|c$fQJ#~7I&Lj~Zs76gi~$EziAa!0iQ5O7OYgoj+-|ClbMG?m%`*8RzmSRL zi;tQ>Ii&unq!fxAowk|Hv*ad`pBg+%Fms_?n zJjvs&@El!2RJ>6bxm2T?@D$x9-+&{}A#1m|KOg?=%%$kJ2EGSaIP&MqPK;}~?QJK$ z3iHqqMlo#<>;}kPMFaq(O8_>;m&gDE-RP$8{A5hLNSt`b_}FG)425_4@?oPi*@t$g zo3o@dTO}zy7FtOkS)^>*@E9o2fI+qX_I<)bvwlOwH9}lH5^jGioqDume@9qid}z^GF7-aH zZ`OE-<>EeJ6m_SAEtMFi4eM)SG`s2lx{&Pe7Ga7`>v@jKZYW+E)J9zvP;AiR(-d-3 zME(5b8_#P*1qRvNrx+u0QgdTtIls8V6Qt9ch?OZ6?3&wosW^ZLG_=@|y9D!{(i|CM zB)|0UJfM{ky(R#)I0ZrhMOl;lj>nqBfDvMN0JEv0rL^!z&6Ifi3O2pd)LxarD><1T zse5t%M}a`madgbAgOyhJMZSdiSBfI9=1I$zemX`0>eXk72Vb_b9ZzTBHTSQcuaF>PKI9Buwbd|^Qw^9+moPhAAmf#|i?%m-j(+fm?>^%)q%ElX zddt__+(^yZZdAkXlD}Fu0lK5=&!5lKUvD|MT6|tyRrH;109jzdLs~vMmO50bv|(*- zVWD!W*LGJu5O434e`Yl(Q#eUouPh#v_nu|-^sPJ~xwAs7$@lEq?V#U7?oQP`@1nH% z=o5sWN{TIB>)N$|uceRyc9%~Ioogj{e^;ywk&a||$alKiI^<&VJdBWRQ=Gu+)IYB7 zOvS&<4gQ+fbgO)ufR-mM^0NjyQmG@AEg@9%emNq$W#g!0dkNXWe56V-6f@fXcWr(} zL4*Kr)Jj3mAIz9c0G06EQ{cN1)1M)auw4{_C4fCa(oW^p3n_XySwMhb%NY^R;nV>l zSdnUFwBh98iL*$B$N}9NQ1ZW9{FJT%f`%2)nbHPyo>+S{fknK(t*20zgvSwM^2EHe z;O}53eM?oE6QjH)@eF6W6Jyo%t;>MK+01d4UgSC#ZQgn|3!8Uz7AQSyq5B{}EfN!y zdOyR7RLRFYwLmZ%^F#M0*IAvNHh9s7dz1y+vIUO9s8$;a%(9ADe9hzM%Yo%RA5p`Z z?nXU-#6?2t-VHFB<#lh8AU3o`KAE$(&zR5O0hEBa{;eOz*_K@QQ5*F`W7BwmZB|rK z2@PSElB`c@Iws@YQvwDY7~4ef)!N>J%ErAv>V$kDTxza#^uH9>oNdcp#O0UpL| zC|`@;YT$OiaUdgLJLixnmreC4*M59mpqwit`Zh3FR^F~fz_?!6IS5*L*h~xddhumZ z{#CUT&~qAY&`(A#C1@`f%QN0-MUDCm~qFJJxZG8;;Gx3)q4}A*_Mc0%Qrrgsncdl`-9PiYF z&2>4J%-Ho84gX~4%s}b8UvfU>Z7KKvX7JC@v%l$uk=|;d+?P+}>Xk{0aIbP)4YjoH zz~g!2yP`Sd+vxX>I&Pw%=cSL2Ykp%%%aqpri$(_%MLw9CMF2~J8nlgk@w>~@*+mMb zZL&CF)MXlArrZEu6b3zUjISxqVr&p5$OzkATKg3>T=WRMKOqZsk&Vv}*+27OJQ@MV z7k9@FBd^JJV7DGo4*WYK(6#J&S6)*7JbhQSLA^&|nlE9aPj2VNZNz0UgC07ks2xE9 z&UH~$u+8d~!^Z+{Q)BJ=V+;RtZ2#=qE6X8-IFVZXajVa_^_$}>fb?3g?3FBVCO=V# zh0vj5J<(*HU?#F>dI_NJ8~$+@fvFpuh{?3rz=_%PJ4$h#V(3OHpe&eW^?uYQE8(&8 z{`|b4scC!%@Sv-*IIImbxAn`VQK;XGB?yn@#a~zqq%948G%8Gn`^Msu>6Qnw1M6B< z%5l<%!kJl~lpl5};Ag*&%RnElo@!-_i^;hlu2N%=y^;YjzH~oi3L@1Q?}{30PO@}PC4QQSR=?7pUx z@@YgaEDV9{(2`7bXeFy{3ym&IY;o~V^2d{R`1=D1;}ebhb9&t5&376KYo>j~=wA<( zMo8MYvNEujn*dD|gmr7DU5PIo=WcxxxIXQ|VDzkkaja;_E@$ECSt-s)ydxVepldpP zcZ62!wQ)_qtLSnG^%}GA54{zVSP1Zht0bB}5)Gb#SgrF*NrD(LBBcC(mEXxhf0 z>C24cp2=Pw%OdhhZ&j*LQ!&eG5UO-ywBF_#bsffH95bhO z>tAy-U`o{@-gy_}9bgRBT=B9gRai=4VS^zo>Q&$|WfJn8Gfen}7@v-AxWRPvyLX&f z>GcEugnVZR?AcHKY*?}{!T+=J;J!xLaY1Te`&-q9(Z2nYJ*B936J+hMy3dn zHrX;z`e+O4VRNRNgg8Nl+rpyh@1Vs?;Jt;sObtZw^Y3>;-%>nO&cuio1*neWU5vu1 z_`(hCt8Y$Qq_6PXe&iadF*KSGH5iVygEl8bIB+P^Ss8V@1jQ}&#zD`+?#Gi$DV;al zi7b@P<`m3CAiW%|z;9M}S9cAV#fBHk4j*9=yZ?V{knV@U*cB0G1Oy7Z)cL4x9C1#oa{L9xiZZu}ur_H@z~F z>t4Z!MHd9Y*ycSc00k{my4J_FUD0Xc75>9l8u8;0hWk#%&Z;Okx5E*u6pQ}^?!Ujf zHAI{Y^Bj+ebWTtmKWOsA$~}?GkPFQ>SlsnMCRzi+C=h3bd7%3Gwv|1@CFH_~2}gHn z9M(MuDyX@G_B}BG(s+hH%lfu5^Fsw>p?)S%($K7UkW*WoQ$0~#pU%fK<&g7cv{93?lPX#`Cv?;apg&e@fXBfqvX)X_8kOkT8_GAGIoHmex_Nd&(T_?>LwYjbou zONP6<7Z<5@w@SPfyV=iFlF;u-G>q&gCwlR?+Y+swQkF*lig7#yZ5=ATckW| zq)e+Vm_W<+;cfD_KP|6is7w-);FR>!d>=3l9*>9wtj#O1*UNR|`-GT)`^L6p>#V!PQYxn2{Y~|f4|>XEOG#dm zfmv!_dTpvQgTB+Ud}^t2dWv$$YPk80(?1Q}`R>fTXw7wz5`L=xue@@OFn}il?ak@H z=1Zq%o9KOVv^HAtKunCGBb*uRQi9*=Rr}VHVzT8Tl=x!zqrcTx5d88inR12+wk%^ukT&pyjKv9)%>nAFb*`44`)6fOjP z*G)4$NU0CL$iQ=Z%xrUcZ&&MN5HU`B3ffKk++~v3jND(PY@y~o-l?kP7Xy*dY3Wd) zAY0FEbu`2(itN29mv)a=})N|fNWst1Pu z|If`8f%ZiC)CN4-_TFI>bNh+y7TW$^P|{3~o%Z#`3usND<$c^(tI_ZGnSCdM3oe_R z#ziWjYfTD|^(t-0X-H{PB)vsbeQ+zd(P%lgf!wDQJWkRtn3@!9FL7-z%N{uAc^!YJ z5a1_}YdhK~xBqqIc0Ty26<)uc*=3gE5-TIOk>T^&y)+X33@)9aVxsY&4#FzwxY_2I zPA|JGhHlm*q4yS;s@=@6)S5QZ#lTbsA(p$&mSXHRz_>e{rMTlb$9z~y&DZKK?%ju; zCo!Vvb^H85pNCZqlDyFf9p0}}{}pKe$~%`6{LoMooj<1DVna_xJ|A(He{gWn5g;6E z0TaXOtt%Sho3kID8+S3|5p^FE9JyC=FC^F6i~C^6qKA4DxG8u%@Vkh4Yw)2ye)L6a zgn6hw@*Eo}&Ar`R*=N-6GxcC1z7%F6&w6by@X^P%Fzx;Fmid#@Y+j5bdT%(as9WqT z9Z3|P*bSkpvIgak^EZn;R8ekUvOh8PLS}o;kJcRXAMeQbwu=e)(qf8MIWJj1NPI!9 zE2!5r#bI}>aJ0>Ma!YkhgubC>E#yPl2Yub~xltsKHG{;x7Q<0EBq6(eT;Z>l?0*4| z8xIL4X%R8#98czh_w|G5-LKvo^`7EBSS5`u*oiR@kC6lSN-3Gw zn#H$y9Us^7OXu}(8t%+NdA|uaZ-t6p`N4Lrii}IWFOn$Yr;dzm#!bV_-pjy?KaOuZ z{zanqSrCfO_7cdBi6caE6Nm$htnIz5cF$T>EZvb1s2NN`x zuxc^bygT0#3@zf+is-M}APjZ6s)4^b_F+cBI0}g0U}i9Z`61No%h? zbURVA$Gf|HJlP1m8i3L|)K?oaD%`!V_s3fJCjtK?G<3$u{0hZs1_aZT2)v8R0^S*M zuTw!PKKmA1sO0=goI%ID(us%0(P!Vp&C7cJF%* zJJp4Ei)<>GspunOd>-1q;@@alDSlLs8s7go8#K2vIQ>5UWFz!rYCKUn zXh^N`!Ec|%e-L)-7@4M2c3G)9IEYs>;*6CrBk?-gk#HsPMv5*7ads|OA&j7&8+9CR z=&7dCDeuE?b6_nQIr^?;d*8+{OGIQ!(RA1SIZpbY9!*%#1btqHqIx9{Z5X|>r9T~2 zSgaF5uNxy_PcgmeE0~2|n3QM0T)FmmovvHaaJR?VX7s;9y!|0Q06GD(&BrLn@+^sx z-V_-s7tPZUQPT&^_S7IrNjfP5eCDWk13KgcGo`e}k<&sq-Lg4vS+$riozeKTn^QX% z2*&GF2J;m)k~dFk>8qB*Qi4nlk;AyEM#^U?hyh@Bm?+GpUQJKiyUtM0jX4YTfN{#l zSV9{v`u8EZ0r5}4jK2=>qGVYb(v6d6B0u*lf?pzF=G<*JQMp9(A_bMA2G}( z=>1CRvl$?2+$jZFjD*I#uH?*lt#dBwZSYZrB`(=&+nLdKYPI|7VbHmZtDbG6pkE&V zCo9cieCdKy{<@Qq6;lGRbtECqn~_8z-Q`4~jXvBt(iuq|*mkNKyrKA;!L5C;)Dd*Q zq9!$5Kw#Yju_58W9hfm23 zaf0I}d*OLag`Du!g8JRmFikdzdw#1?KF`Ab_o|g)<(b8UCTQ`xcM96&htYfLU%QPD zNhs>JI@OZOvi~Sw`ifeyh6iqEMYI*!_vwjU*bjqmb2TlGs8290HFL2oc35)cyA9Lx z(#H87hbJqv*>n3JP*R_kR8mt@!%DmsIJbZchIol-??osOJf*nnlwU7i9w`aF#MLch zb8G{_XMDV7EEtRyJ^Uqmm4;o2;V_}NFt8|6a_;7WP+rS+o$9XHDE|5zq@rtLO{jgI z9BciZW{0D$H#;*y^9jN&)V)o39mg^)yp6v1h6pouP4Zfc1F~!%`8oXS)Q<)e400Dr z#a+^c+OFTlB}&|b9XdfDfn9Bza)xJ}bY`5tC$9X0TFB)~1^{qfjuIr%UR zv04+b#A>7(Q&n>^7MHYoRKKASmo%(Z+~FOw_i7t;G5~^cwC2*zH2yoI_rF3==Y_Sp zYBoD45gC^HsC5uA_e@BHk?w}uuhx{m)`jI-z4LSjy7|Uemtv2R98!7m{GuK^%a8H< z`?^PSylU-XDMfGgdh@{|#`KT;B|KT1_j_~ZT7N(Tpw~4sXVQUY*pQ4Y#m#O4*<(Gy zp?jO1-WPcEaVFA!{R5JfzYWFGlq6mDq({jhb&A^@{vexo*cR?t6W+C@I$( zPwd>%J#-^RmC^9$>#EiM_kygE@;qbOJr%i9gx+?0dBZxVMRUP{+cf?qVa&|11E9g9XYs`Je?1<==pUi)kM#*d7J+P!|2{Z<9rbI;*19M%tdE7QCf{;TSaJEmlNgXpXRY3vE);y20Y)J z4nyEYmmq{sG)w9Yc@XQ8M%wGC6E%f!Gwiz7Dq?tVs%lB$C@9<~Plxvh>My8yi!X-I zvt;9a?+>rSESh#{IEKvNOsbXmX(Qs+4m7- z+w^-+56@Vl*v8FpUyQwQ`-j5|pLB@_<=?%I`1Eg50pmI7ZI{0Z(AuUGb~GO?@FHMD zO-mjn)Vh_1fKZI%63@Uv0_19QKxcQ|tNO@WF+M#KmK$ zKj_>1*6PyQ-ulT*-GF<`;pF~aO$!KSy$}iAnR;ExnnUq$uv2z5r%s%C&<^KZ>EAF> z!>i)=V!Bz#=Em<$X-bgNCsM1Bmu;BC;FOP5OJ8t9#1hCG8Nqpk^GX)IUd}qO9EI(*WtWqm?V_^?x6{P-ey*)#tq$D;z_hRDc}nAh47kv;7$K8wx|q-gt_4={sp{LM@RYpZ3%6nFJ6kA(kK z+Pi9N*RRiuE_wGy$lp;4-0y}L?7c|~hWqPz&+yc&(eY=)#n zS10ci755Ib)TZZFRZTNp@9_@reADo#&!}nj|75@Bt2bcGP%(Yd`A@?Vj+V#SA3xUn z*cOrM9>+B@d)(XYt=^|^Trqkwy>^q~?Z0c->UVM3r-q#oEVNRagxYq*9!{V5Bun{9 zhB1m&_krOd;o(}Jxv6%K_NI*hCm|S$C^+~rh)b{V;42Dh?Mbe*2M zdt`l_z6ZomT7W20Hmr4c$zrLn1QLnp%%EKmg$3Mmji{00I1VZ7@F9nbUgV>XbC1IR zst}C-A?}VACpjPMD$3&08Zj1w<+_hpAs)cVM>$Ur=GGB7Es;eBf7is$& zf&YOF#zn;J=^o2MEh2lMaq5~L=J!C2RFFhkw%N`IG7xL+oc@TYS!8s`bn~v0N6e-k z;-^0AkCrk0+EK#>tw8J~>dQ1KUNtNcjXzBxyiGgm6W-X#GQc=1#GObn&Cis`d}#A^ zZOyCE*QfcC&4<^i;ZWL)`uHBTJZ?f%TB#Xa_inD`l4_PonYBAmt?J}0(Rh@k`W_$P z@Pi_8J$W}fQ{;Kn{UUptl2@H)^iV%VK4u@)<#O)lyzo1hAxCrnH?%gsLhSM-$1l!} z!^h5@ne@w}3)#wW?;g~yVm1!hh^s^`?E>d+*Cc=aXYm>w|`fL{$gtg;%^U6V?yD@*8hy=GaZf65lT0i3XN-LafxGLJ=jw-4gB zFFo3Z#KV1hKK1(dw(LxK8s(@kbq@j9G{&A&45$VJ?$1yq#@hMj;C1Saa4dWZgvVkI zx1jJb(xJRUks7g*pWcnVb{MB(okaV+*!4PY-VC1rzR^xv4bd*`v zL{}YA2tYNtT-i&p{cBpDG*Humv7Ea?RV4`$9+8B>N2N-X^@1pU{R-S`5pJ}rYXdje zFB!r)LgwpzTUywfq>b4nlhSQb9&odlh>_P;`#b3jz?mV$bNE>&-CTm02ULF@Kq2|l zR3!c@-rIFC0o7W22XQ@hf(<=OCLW8k7W9(nxrqXdd&s$ro*%lWrY~I`0_LB?yRy0> zgir3yP<`f??(~0fJnqzR;S@QQbUa;<4MNQCl_#T8m@_pGGrZ%1RBL0$*FheUOmRMwdQ(?#ht#5;is(}d@7vI45an!jH_2HX*Q=r0}PI^8Rd(HJ=U8}bv#R$ zcR{O7sS_Q$W|L_#RcZ&U@Ick3UJ5=Zhe)2%9gL;umOU<^QfAb*`hNx!*%5m+6?&4^ zn`^J0WAm+;qHHnTTbA7@apgNLy;3ghpD^X5a4CU_-Up4Lkd z{b4;;)IltVsq8Lx(4&&##5XS{PmF*tq~6)&9uRYk+lalS7l_T;rU;GRNE7eWM4hO5 z&E<;1l3(k1u_hcY;+))@QnPX8c30qg&-0^QRYX}3$Y=-HN7Pp^f$Sx|*TQdKtmv1# z|2=u_KP>qTLVqvvqQ;UWOn@Tj394Sdq9iAFH8F`4^kkF8nD$?=aUMX-){_;(4-E3c zI`T2e;R}*RoPG*2yw6;DC%}zb(%;*mw6x7$sH?}`Y7=aD%IkgSZ3~Zt?(M!;zkg_$ zCtfq)nSpJ~+fE!$v0w2XD(ouJqTSCi0iI8k-Q+AL-?&ZyyNmnBXuL@DN3rv2^ZMQo z4w%Y1oO`@dxdmb6>q-ECY_Dq@(U6ov&Qb*#Ii}AFsi_#rF%d-MFPrU|79Loy=Oo)r z=N2Rm$Mg_`sd&Euv#gCA!4m7|5+ioRS4MORPMBkA((`y~qUDV>NwytzI$o;eV;<<` zq5V-Gi_%yb;jyBv3fMNd=X^fK+u<#={?O&%gu8`T(xMw3GzGn>fRtuKSSL(?4d!+i zu+0tJruPU#_Aj4eQ-<5EGByD-kkQENo6|{Z8}DXBb0%90%}4EwN_60sEv8w~cBFLo zN<30NdTJ*TL6aBBa@HmbY?oZmklopeELZsz$4j7adKJYfDT}0_VvSWZkEW(ZCc+Ds zoBd8sp0D0Rh)n-n6jdjd1GV}}iJBkU%jKbcIkozN<+#+ZK!NNL5xEg$z1Ml%4Y1K0 z;Y*%K5vkQFrFGvu-f7w<$^*k^kRKHM>PF~jbL{Rx9HIcR=Mg_fF&0OAtJM<1=5hK! z-W)STtfXztp4KC2lC+!C-(M4g8^a{0hqUZdu^Jq6+v<81JnuDgqTLppjbAkD>W6OS z3#U^5jnn&{5P+YYzK|cOyJ%Q(pw%4?+RWbV7|B z>x`4dtK-ImyX}wbs1iHa;iWVG7@HkZeO-O|Y&J$B=Ip)LB3epp80i9!z?`V z6tBrTk?nw_v}&V)E09Dzm>uNj4+w-vbh92D8Mj`NitE~t8y4yJB~Ur{Y@?RttrPw| z_bsR4PI8JxIfoF%n$8b-^*nKzcL)=Zr2YbLDIc3Sf=X=`j{>jh!t1ouMd8yrO!#wOzqEGF~_R+>x*Hv<-?!-mUVfEjrv9ChR1e7{HyuyNh*k$Y&)#T5bsyM32iz<{r02IkPuGF%)@m)x&!fYOG3q zG(p{*9Vj42nY~uz^~+;iF>k;Uu=-KM{h6Rg;%qf#{C&u}37 zAn1KCGsxC&zIQokXUxDnu6@1@eBH=4X;rHwNp|?#;uR45V=Lut9gfTJG}-_OJoiEu z&-q7rWL@@)(#lFr2Q)_@gW4WZ;-D8kCbWS&wH}S-(b{WA?xlNozhBMJ(niZ<{6~P* zltR~=Z1J@6=+S@3tfs*TyO!+6)dIda@7A;0&if}Sc8uM5yN)Jsszw3V^bTu}1@hg| zpHPci<7qqZj=2ECSi1|<&p zX}tG&T8-wGv|0**BY+xy;fk!h!XH4oobX`3m)DO0^uE>Rj=}4eQnQb84>Qq83T@b=CX(cb7>gv1M@~qsaP)U$}^3NSU z4vrS1dv(~GCIs!shS++1&ec~cQs5ui9z51<%=Z}}4A9F^6D zdK80LUDAB1#}aW!J1>kqs&^L{=j&ulMt?rpbJ!S>mA6D9H*K{2_8%Q`aBf;DEwGNZ zR`@81EK zqUN}E(XUR52E(}w-Ex%~QE{;ee1yBV?>=e`}}|{ya*iz zTnK>F>tBHf&m)NR(l@Y1F$2F3x}_ z!U!aNv0QI!OND&kZwRQi9vWNx;lqY6WaQ4kvLL~o44N;4<)kSDxuzK1q9i{AsG-tL zag**(oxF88g>9N6t;B(CbOi4tz zaxI^a;UB@)hx!HvTJMn?Tm}4Gmxc@Dx3*Q7StwqgL4y*8#oW>pc_Vh)WIdE17pso( zm)Jw+z0HJ&t#C^Z_c&)6t_5r)#i!Mn#FV+4d#KZ<+aO#1W|?j>N7ODFSa)K zV&%dt@Ks6R(Fi^K@Ka5MZmqe=mc@x`pwyWGWa~5j&l|RPLeUsg4+*n3`bL&aI9MC! zMUHBe#}#aJ1Lf-JiIg@@9~`mx85hED61?S%k((F(CT8LEumPo@a^aU-`xWj#0d)*M z0aA)%KYK%T*0op-nNEYiYy-YZgV~7!5pl&HMut)N#K%GUGuT62_$SFH$An8^mYQs> z-CJ!+4}&$9Yp&~T4>{_M$q`rW+*LmEI4dnczA4@3MXNgyQNyF335CKR8@A4E_haOW zOB_!j1Ak~VeiH}P!7kWmCf)_~we#%2{OD*nS&rBK2nf1RRV-V;yM)5$rDkS_cJ9j$ zIOB2>BhyhY@nKCmxF+3?K>V-Sq=TYH=8ahF)YqB$)|Hyu76G@q5e$IDt_y82xRJe= z$=7A~3Z^h8uEZ&bF1)n}vGhMf8b$s%Z?>E?$cV?23w%csRrKhcH#i5X9Q8Lh7J2D! zkvb0F7GclBh)amcGTq6o@JHr?HA_W@TrJhe3rC12kN20o2w)iKckeyT^{@@Y)=OjI za;^ogK=Ptbz=ucV^EfBT6@UGCXTn)X5`z<><{|Q;?Qi6Ivvt#iWo_ha(iyhuNzZ&# zuUA6-a=>*JE083Uvb|urn{Qo!nPO>>FnlRN`)Su_ly6 zhCqWrYf1Lb3x~PNhdHDuRC&9TYio$n0Wb3hp;J}N*-QVkn|*zj)!Z2n2wUJ3$#6%O z?#$fd5Wwc7zBLaZb;^znFC~L^&zN4g4<$th_z|qujuJ=x^~ev7DJs2htB?$mUHQ=N zQ+N)pN23bXUImV;9Id?~=Qm-9Vk866PxHmz>^(i)6APqv7=7S{qbnVGj@^rvC}0R= zfcRhWy&=dHMmnfA-^!sK4_Lt$@OD%t;DEjN9yn_^vZj@2=UwI}N^!^MjU334%IfFR zG&r!vGORYWDi^u~rGSs#%SE?ie!lO%HrfwAb1!`g|3kPk3{G5>PFKP~9lfp9=u9Gr zcsrm^f9Iy4SoR(Ruq|+_501vR&wk>j;3aNTJRhZ4G$c3(@02qOdRyOwev>+!NX8w& zVdQ+NbjKG*m7{zVoQAJek>Eh2sG-SxS+c_3wLfEacZfisNP< zX6vTKG~WA82_H+of2D7rmDHPjvG1|Wwe7^7HeV74V?EdBC zj<(i>2-fT+9ER-=r)7GJm(?|Bh`n~)+({iNIo{?s$8!3VbE3X6l}y#LXl9AO6DBld zEvfk$kxn|HPV{XRSfuu;7Z+xda52+bf+#1lbDu+jq@M!C z7AToUhP;UOrc-!PEQZg~(C1HFb?~iL%Q>RSsq|dYV;@nu3gcZ^5 z(m(DN+16B08LF_TUd7+k%f?zEfZ(yiKLeLv3J?}cnv`oxg2}v1`>ls%ui-3eORAa_+ z$HEN&Ka_v6Xu+vGxUlscXh_4=lVg=#H!6?c(n!>bY`tm`#`KcZ17X(u%J{-AW+YY~ zw@f(FUM>{P7Cxy5q4e|yYg0X+A99H77_4tD|A9Z9J$h=}t3fn`U76>+-bNdu=Q<-B zUlEIk!hhC8(%8o}eksqp=@jW3OvhEj5iUWtwgg`q^nYOpn1?=uM$ap`VdkEQxYeP! zmGz`-G(8*o*0-gP5Ha5+Tm%sXNJ{|dkd;8|@q7g^E5Bd6z+wN9FzS$c%^3NM8js|^v4u4<&Z*Luq20|PE>1q9UBmbqy gzh+}JdPuE%rKK$^?nucuTEO2icg#n0H^SwA0#D-s_W%F@ literal 0 HcmV?d00001 diff --git a/docs/en/latest/architecture-design/apisix.md b/docs/en/latest/architecture-design/apisix.md index 558e072b668e..f5d262e8dc86 100644 --- a/docs/en/latest/architecture-design/apisix.md +++ b/docs/en/latest/architecture-design/apisix.md @@ -21,6 +21,10 @@ title: APISIX # --> +## Apache APISIX : Software Architecture + +![flow-software-architecture](../../../assets/images/flow-software-architecture.png) + ## Plugin Loading Process ![flow-load-plugin](../../../assets/images/flow-load-plugin.png) From a65aa09c6ad733d772409ec1a118f3927afae91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B2=BB=E5=9B=BD?= Date: Mon, 6 Dec 2021 10:21:06 +0800 Subject: [PATCH 161/260] feat: support resolve default value when environment not set (#5675) --- apisix/cli/file.lua | 12 ++++++++++-- conf/config.yaml | 10 ++++++++++ t/cli/test_main.sh | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua index b4d51fb2e792..5d066e0dadcd 100644 --- a/apisix/cli/file.lua +++ b/apisix/cli/file.lua @@ -65,8 +65,16 @@ local function resolve_conf_var(conf) local var_used = false -- we use '${{var}}' because '$var' and '${var}' are taken -- by Nginx - local new_val = val:gsub("%$%{%{%s*([%w_]+)%s*%}%}", function(var) - local v = getenv(var) + local new_val = val:gsub("%$%{%{%s*([%w_]+[%:%=]?.-)%s*%}%}", function(var) + local i, j = var:find("%:%=") + local default + if i and j then + default = var:sub(i + 2, #var) + default = default:gsub('^%s*(.-)%s*$', '%1') + var = var:sub(1, i - 1) + end + + local v = getenv(var) or default if v then if not exported_vars then exported_vars = {} diff --git a/conf/config.yaml b/conf/config.yaml index b45fa5e513d8..421ac0912aa6 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -30,6 +30,16 @@ # And then run `export ETCD_HOST=$your_host` before `make init`. # # If the configured environment variable can't be found, an error will be thrown. +# +# Also, If you want to use default value when the environment variable not set, +# Use `${{VAR:=default_value}}` instead. For instance: +# +# etcd: +# host: +# - http://${{ETCD_HOST:=localhost}}:2379 +# +# This will find environment variable `ETCD_HOST` first, and if it's not exist it will use `localhost` as default value. +# apisix: admin_key: - name: admin diff --git a/t/cli/test_main.sh b/t/cli/test_main.sh index 771d96846d13..f9c6a386e8b0 100755 --- a/t/cli/test_main.sh +++ b/t/cli/test_main.sh @@ -310,6 +310,45 @@ fi echo "pass: support environment variables in local_conf" +# support default value when environment not set +echo ' +tests: + key: ${{TEST_ENV:=1.1.1.1}} +' > conf/config.yaml + +make init + +if ! grep "env TEST_ENV=1.1.1.1;" conf/nginx.conf > /dev/null; then + echo "failed: should use default value when environment not set" + exit 1 +fi + +echo ' +tests: + key: ${{TEST_ENV:=very-long-domain-with-many-symbols.absolutely-non-exists-123ss.com:1234/path?param1=value1}} +' > conf/config.yaml + +make init + +if ! grep "env TEST_ENV=very-long-domain-with-many-symbols.absolutely-non-exists-123ss.com:1234/path?param1=value1;" conf/nginx.conf > /dev/null; then + echo "failed: should use default value when environment not set" + exit 1 +fi + +echo ' +tests: + key: ${{TEST_ENV:=192.168.1.1}} +' > conf/config.yaml + +TEST_ENV=127.0.0.1 make init + +if ! grep "env TEST_ENV=127.0.0.1;" conf/nginx.conf > /dev/null; then + echo "failed: should use environment variable when environment is set" + exit 1 +fi + +echo "pass: support default value when environment not set" + # support merging worker_processes echo ' nginx_config: From 9754b5b694fb570078d79428724666948ef1d15b Mon Sep 17 00:00:00 2001 From: litesun Date: Mon, 6 Dec 2021 16:13:45 +0800 Subject: [PATCH 162/260] docs: update MAINTAIN.md (#5676) Co-authored-by: leslie <59061168+leslie-tsang@users.noreply.github.com> --- MAINTAIN.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MAINTAIN.md b/MAINTAIN.md index 0a0b2cdfc8dc..ec9a9837ef07 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -27,13 +27,16 @@ 4. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created via `VERSION=x.y.z make release-src` 5. Send the [vote email](https://lists.apache.org/thread/vq4qtwqro5zowpdqhx51oznbjy87w9d0) to dev@apisix.apache.org + > After executing the `VERSION=x.y.z make release-src` command, the content of the vote email will be automatically generated in the `./release` directory named `apache-apisix-${x.y.z}-vote-contents` 6. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/k2frnvj4zj9oynsbr7h7nd6n6m3q5p89) to dev@apisix.apache.org 7. Move the vote artifact to Apache's apisix repo 8. Register the release info in https://reporter.apache.org/addrelease.html?apisix 9. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.2) from the minor branch 10. Update [APISIX's website](https://github.com/apache/apisix-website/commit/f9104bdca50015722ab6e3714bbcd2d17e5c5bb3) 11. Update APISIX rpm package -12. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` + > Go to [apisix-build-tools](https://github.com/api7/apisix-build-tools) repository and create a new tag named `apisix-${x.y.z}` to automatically submit the + package to yum repo +12. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601) in [APISIX docker repository](https://github.com/apache/apisix-docker), and create new branch from master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` 13. Send the [ANNOUNCE email](https://lists.apache.org/thread.html/ree7b06e6eac854fd42ba4f302079661a172f514a92aca2ef2f1aa7bb%40%3Cdev.apisix.apache.org%3E) to dev@apisix.apache.org & announce@apache.org ### Release minor version @@ -42,12 +45,14 @@ via `VERSION=x.y.z make release-src` 2. Package a vote artifact to Apache's dev-apisix repo. The artifact can be created via `VERSION=x.y.z make release-src` 3. Send the [vote email](https://lists.apache.org/thread/q8zq276o20r5r9qjkg074nfzb77xwry9) to dev@apisix.apache.org + > After executing the `VERSION=x.y.z make release-src` command, the content of the vote email will be automatically generated in the `./release` directory named `apache-apisix-${x.y.z}-vote-contents` 4. When the vote is passed, send the [vote result email](https://lists.apache.org/thread/p1m9s116rojlhb91g38cj8646393qkz7) to dev@apisix.apache.org 5. Move the vote artifact to Apache's apisix repo 6. Register the release info in https://reporter.apache.org/addrelease.html?apisix 7. Create a [GitHub release](https://github.com/apache/apisix/releases/tag/2.10.0) from the minor branch 8. Merge the pull request into master branch 9. Update [APISIX's website](https://github.com/apache/apisix-website/commit/7bf0ab5a1bbd795e6571c4bb89a6e646115e7ca3) -10. Update APISIX rpm package -11. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601), and create new branch form master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` +10. Update APISIX rpm package. + > Go to [apisix-build-tools](https://github.com/api7/apisix-build-tools) repository and create a new tag named `apisix-${x.y.z}` to automatically submit the rpm package to yum repo +11. Update [APISIX docker](https://github.com/apache/apisix-docker/commit/829d45559c303bea7edde5bebe9fcf4938071601) in [APISIX docker repository](https://github.com/apache/apisix-docker), and create new branch from master, named as `release/apisix-${version}`, e.g. `release/apisix-2.10.2` 12. Send the [ANNOUNCE email](https://lists.apache.org/thread/4s4msqwl1tq13p9dnv3hx7skbgpkozw1) to dev@apisix.apache.org & announce@apache.org From 9731f51857ad0224190fa48c5684b29a25166276 Mon Sep 17 00:00:00 2001 From: Soham Banerjee <63705023+soham4abc@users.noreply.github.com> Date: Mon, 6 Dec 2021 14:28:49 +0530 Subject: [PATCH 163/260] docs: Software architecture diagram added (zh-docs) (#5712) --- docs/zh/latest/architecture-design/apisix.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/zh/latest/architecture-design/apisix.md b/docs/zh/latest/architecture-design/apisix.md index 1add0d369758..cd2e57bc3689 100644 --- a/docs/zh/latest/architecture-design/apisix.md +++ b/docs/zh/latest/architecture-design/apisix.md @@ -21,6 +21,10 @@ title: APISIX # --> +## 软件架构 + +![软件架构](../../../assets/images/flow-software-architecture.png) + ## 插件加载流程 ![插件加载流程](../../../assets/images/flow-load-plugin.png) From 49762bcd7509448340ea0d405e0230e99ae0a8f5 Mon Sep 17 00:00:00 2001 From: yuz10 <845238369@qq.com> Date: Tue, 7 Dec 2021 09:14:36 +0800 Subject: [PATCH 164/260] feat: rocketmq logger (#5653) --- README.md | 2 +- apisix/plugins/rocketmq-logger.lua | 256 +++++ ci/linux-ci-init-service.sh | 6 + ci/pod/docker-compose.yml | 24 + conf/config-default.yaml | 1 + docs/en/latest/config.json | 1 + docs/en/latest/plugins/rocketmq-logger.md | 234 +++++ docs/zh/latest/README.md | 2 +- docs/zh/latest/config.json | 1 + docs/zh/latest/plugins/rocketmq-logger.md | 229 +++++ rockspec/apisix-master-0.rockspec | 1 + t/admin/plugins.t | 1 + t/debug/debug-mode.t | 1 + t/plugin/rocketmq-logger-log-format.t | 121 +++ t/plugin/rocketmq-logger.t | 1098 +++++++++++++++++++++ 15 files changed, 1976 insertions(+), 2 deletions(-) create mode 100644 apisix/plugins/rocketmq-logger.lua create mode 100644 docs/en/latest/plugins/rocketmq-logger.md create mode 100644 docs/zh/latest/plugins/rocketmq-logger.md create mode 100644 t/plugin/rocketmq-logger-log-format.t create mode 100644 t/plugin/rocketmq-logger.t diff --git a/README.md b/README.md index 1591e542230e..b3c8909bcc00 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - High performance: The single-core QPS reaches 18k with an average delay of fewer than 0.2 milliseconds. - [Fault Injection](docs/en/latest/plugins/fault-injection.md) - [REST Admin API](docs/en/latest/admin-api.md): Using the REST Admin API to control Apache APISIX, which only allows 127.0.0.1 access by default, you can modify the `allow_admin` field in `conf/config.yaml` to specify a list of IPs that are allowed to call the Admin API. Also, note that the Admin API uses key auth to verify the identity of the caller. **The `admin_key` field in `conf/config.yaml` needs to be modified before deployment to ensure security**. - - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md)) + - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md), [RocketMQ Logger](docs/en/latest/plugins/rocketmq-logger.md)) - [Datadog](docs/en/latest/plugins/datadog.md): push custom metrics to the DogStatsD server, comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server. - [Helm charts](https://github.com/apache/apisix-helm-chart) diff --git a/apisix/plugins/rocketmq-logger.lua b/apisix/plugins/rocketmq-logger.lua new file mode 100644 index 000000000000..bf7adc5724e1 --- /dev/null +++ b/apisix/plugins/rocketmq-logger.lua @@ -0,0 +1,256 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +local core = require("apisix.core") +local log_util = require("apisix.utils.log-util") +local producer = require ("resty.rocketmq.producer") +local acl_rpchook = require("resty.rocketmq.acl_rpchook") +local batch_processor = require("apisix.utils.batch-processor") +local plugin = require("apisix.plugin") + +local type = type +local pairs = pairs +local plugin_name = "rocketmq-logger" +local stale_timer_running = false +local ngx = ngx +local timer_at = ngx.timer.at +local buffers = {} + +local lrucache = core.lrucache.new({ + type = "plugin", +}) + +local schema = { + type = "object", + properties = { + meta_format = { + type = "string", + default = "default", + enum = {"default", "origin"}, + }, + nameserver_list = { + type = "array", + minItems = 1, + items = { + type = "string" + } + }, + topic = {type = "string"}, + key = {type = "string"}, + tag = {type = "string"}, + timeout = {type = "integer", minimum = 1, default = 3}, + use_tls = {type = "boolean", default = false}, + access_key = {type = "string", default = ""}, + secret_key = {type = "string", default = ""}, + name = {type = "string", default = "rocketmq logger"}, + max_retry_count = {type = "integer", minimum = 0, default = 0}, + retry_delay = {type = "integer", minimum = 0, default = 1}, + buffer_duration = {type = "integer", minimum = 1, default = 60}, + inactive_timeout = {type = "integer", minimum = 1, default = 5}, + include_req_body = {type = "boolean", default = false}, + include_req_body_expr = { + type = "array", + minItems = 1, + items = { + type = "array", + items = { + type = "string" + } + } + }, + include_resp_body = {type = "boolean", default = false}, + include_resp_body_expr = { + type = "array", + minItems = 1, + items = { + type = "array", + items = { + type = "string" + } + } + }, + }, + required = {"nameserver_list", "topic"} +} + +local metadata_schema = { + type = "object", + properties = { + log_format = log_util.metadata_schema_log_format, + }, +} + +local _M = { + version = 0.1, + priority = 402, + name = plugin_name, + schema = schema, + metadata_schema = metadata_schema, +} + + +function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + end + + local ok, err = core.schema.check(schema, conf) + if not ok then + return nil, err + end + return log_util.check_log_schema(conf) +end + + +-- remove stale objects from the memory after timer expires +local function remove_stale_objects(premature) + if premature then + return + end + + for key, batch in pairs(buffers) do + if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then + core.log.warn("removing batch processor stale object, conf: ", + core.json.delay_encode(key)) + buffers[key] = nil + end + end + + stale_timer_running = false +end + + +local function create_producer(nameserver_list, producer_config) + core.log.info("create new rocketmq producer instance") + local prod = producer.new(nameserver_list, "apisixLogProducer") + if producer_config.use_tls then + prod:setUseTLS(true) + end + if producer_config.access_key ~= '' then + local aclHook = acl_rpchook.new(producer_config.access_key, producer_config.secret_key) + prod:addRPCHook(aclHook) + end + prod:setTimeout(producer_config.timeout) + return prod +end + + +local function send_rocketmq_data(conf, log_message, prod) + local result, err = prod:send(conf.topic, log_message, conf.tag, conf.key) + if not result then + return false, "failed to send data to rocketmq topic: " .. err .. + ", nameserver_list: " .. core.json.encode(conf.nameserver_list) + end + + core.log.info("queue: ", result.sendResult.messageQueue.queueId) + + return true +end + + +function _M.body_filter(conf, ctx) + log_util.collect_body(conf, ctx) +end + + +function _M.log(conf, ctx) + local entry + if conf.meta_format == "origin" then + entry = log_util.get_req_original(ctx, conf) + else + local metadata = plugin.plugin_metadata(plugin_name) + core.log.info("metadata: ", core.json.delay_encode(metadata)) + if metadata and metadata.value.log_format + and core.table.nkeys(metadata.value.log_format) > 0 + then + entry = log_util.get_custom_format_log(ctx, metadata.value.log_format) + core.log.info("custom log format entry: ", core.json.delay_encode(entry)) + else + entry = log_util.get_full_log(ngx, conf) + core.log.info("full log entry: ", core.json.delay_encode(entry)) + end + end + + if not stale_timer_running then + -- run the timer every 30 mins if any log is present + timer_at(1800, remove_stale_objects) + stale_timer_running = true + end + + local log_buffer = buffers[conf] + if log_buffer then + log_buffer:push(entry) + return + end + + -- reuse producer via lrucache to avoid unbalanced partitions of messages in rocketmq + local producer_config = { + timeout = conf.timeout * 1000, + use_tls = conf.use_tls, + access_key = conf.access_key, + secret_key = conf.secret_key, + } + + local prod, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_producer, + conf.nameserver_list, producer_config) + if err then + return nil, "failed to create the rocketmq producer: " .. err + end + core.log.info("rocketmq nameserver_list[1] port ", + prod.client.nameservers[1].port) + -- Generate a function to be executed by the batch processor + local func = function(entries, batch_max_size) + local data, err + if batch_max_size == 1 then + data = entries[1] + if type(data) ~= "string" then + data, err = core.json.encode(data) -- encode as single {} + end + else + data, err = core.json.encode(entries) -- encode as array [{}] + end + + if not data then + return false, 'error occurred while encoding the data: ' .. err + end + + core.log.info("send data to rocketmq: ", data) + return send_rocketmq_data(conf, data, prod) + end + + local config = { + name = conf.name, + retry_delay = conf.retry_delay, + batch_max_size = conf.batch_max_size, + max_retry_count = conf.max_retry_count, + buffer_duration = conf.buffer_duration, + inactive_timeout = conf.inactive_timeout, + } + + local err + log_buffer, err = batch_processor:new(func, config) + + if not log_buffer then + core.log.error("error when creating the batch processor: ", err) + return + end + + buffers[conf] = log_buffer + log_buffer:push(entry) +end + + +return _M diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 83144b7b3b30..0c3ff5d03096 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -25,3 +25,9 @@ docker pull openwhisk/action-nodejs-v14:nightly docker run --rm -d --name openwhisk -p 3233:3233 -p 3232:3232 -v /var/run/docker.sock:/var/run/docker.sock openwhisk/standalone:nightly docker exec -i openwhisk waitready docker exec -i openwhisk bash -c "wsk action update test <(echo 'function main(args){return {\"hello\":args.name || \"test\"}}') --kind nodejs:14" + +docker exec -i rmqnamesrv rm /home/rocketmq/rocketmq-4.6.0/conf/tools.yml +docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test -c DefaultCluster +docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test2 -c DefaultCluster +docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test3 -c DefaultCluster +docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test4 -c DefaultCluster diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index c71ab63d6dc6..2dedaf9dff80 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -355,6 +355,29 @@ services: networks: apisix_net: + rocketmq_namesrv: + image: apacherocketmq/rocketmq:4.6.0 + container_name: rmqnamesrv + restart: unless-stopped + ports: + - "9876:9876" + command: sh mqnamesrv + networks: + rocketmq_net: + + rocketmq_broker: + image: apacherocketmq/rocketmq:4.6.0 + container_name: rmqbroker + restart: unless-stopped + ports: + - "10909:10909" + - "10911:10911" + - "10912:10912" + depends_on: + - rocketmq_namesrv + command: sh mqbroker -n rocketmq_namesrv:9876 -c ../conf/broker.conf + networks: + rocketmq_net: networks: apisix_net: @@ -362,3 +385,4 @@ networks: kafka_net: nacos_net: skywalk_net: + rocketmq_net: diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 7cf130b256cb..7be15f9267b0 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -349,6 +349,7 @@ plugins: # plugin list (sorted by priority) - sls-logger # priority: 406 - tcp-logger # priority: 405 - kafka-logger # priority: 403 + - rocketmq-logger # priority: 402 - syslog # priority: 401 - udp-logger # priority: 400 #- log-rotate # priority: 100 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 8a775e099b29..72f027f0e91f 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -116,6 +116,7 @@ "plugins/skywalking-logger", "plugins/tcp-logger", "plugins/kafka-logger", + "plugins/rocketmq-logger", "plugins/udp-logger", "plugins/syslog", "plugins/log-rotate", diff --git a/docs/en/latest/plugins/rocketmq-logger.md b/docs/en/latest/plugins/rocketmq-logger.md new file mode 100644 index 000000000000..f17968c4dd51 --- /dev/null +++ b/docs/en/latest/plugins/rocketmq-logger.md @@ -0,0 +1,234 @@ +--- +title: rocketmq-logger +--- + + + +## Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**Info**](#info) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +`rocketmq-logger` is a plugin which provides the ability to push requests log data as JSON objects to your external rocketmq clusters. + + In case if you did not receive the log data don't worry give it some time it will automatically send the logs after the timer function expires in our Batch Processor. + +For more info on Batch-Processor in Apache APISIX please refer. +[Batch-Processor](../batch-processor.md) + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| ---------------- | ------- | ----------- | -------------- | ------- | ---------------------------------------------------------------------------------------- | +| nameserver_list | object | required | | | An array of rocketmq nameservers. | +| topic | string | required | | | Target topic to push data. | +| key | string | optional | | | Keys of messages to send. | +| tag | string | optional | | | Tags of messages to send. | +| timeout | integer | optional | 3 | [1,...] | Timeout for the upstream to send data. | +| use_tls | boolean | optional | false | | Whether to open TLS | +| access_key | string | optional | "" | | access key for ACL, empty string means disable ACL. | +| secret_key | string | optional | "" | | secret key for ACL. | +| name | string | optional | "rocketmq logger" | | A unique identifier to identity the batch processor. | +| meta_format | enum | optional | "default" | ["default","origin"] | `default`: collect the request information with default JSON way. `origin`: collect the request information with original HTTP request. [example](#examples-of-meta_format)| +| batch_max_size | integer | optional | 1000 | [1,...] | Set the maximum number of logs sent in each batch. When the number of logs reaches the set maximum, all logs will be automatically pushed to the `rocketmq` service. | +| inactive_timeout | integer | optional | 5 | [1,...] | The maximum time to refresh the buffer (in seconds). When the maximum refresh time is reached, all logs will be automatically pushed to the `rocketmq` service regardless of whether the number of logs in the buffer reaches the set maximum number. | +| buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed.| +| max_retry_count | integer | optional | 0 | [0,...] | Maximum number of retries before removing from the processing pipe line. | +| retry_delay | integer | optional | 1 | [0,...] | Number of seconds the process execution should be delayed if the execution fails. | +| include_req_body | boolean | optional | false | [false, true] | Whether to include the request body. false: indicates that the requested body is not included; true: indicates that the requested body is included. Note: if the request body is too big to be kept in the memory, it can't be logged due to Nginx's limitation. | +| include_req_body_expr | array | optional | | | When `include_req_body` is true, control the behavior based on the result of the [lua-resty-expr](https://github.com/api7/lua-resty-expr) expression. If present, only log the request body when the result is true. | +| include_resp_body| boolean | optional | false | [false, true] | Whether to include the response body. The response body is included if and only if it is `true`. | +| include_resp_body_expr | array | optional | | | When `include_resp_body` is true, control the behavior based on the result of the [lua-resty-expr](https://github.com/api7/lua-resty-expr) expression. If present, only log the response body when the result is true. | + +### examples of meta_format + +- **default**: + +```json + { + "upstream": "127.0.0.1:1980", + "start_time": 1619414294760, + "client_ip": "127.0.0.1", + "service_id": "", + "route_id": "1", + "request": { + "querystring": { + "ab": "cd" + }, + "size": 90, + "uri": "/hello?ab=cd", + "url": "http://localhost:1984/hello?ab=cd", + "headers": { + "host": "localhost", + "content-length": "6", + "connection": "close" + }, + "body": "abcdef", + "method": "GET" + }, + "response": { + "headers": { + "connection": "close", + "content-type": "text/plain; charset=utf-8", + "date": "Mon, 26 Apr 2021 05:18:14 GMT", + "server": "APISIX/2.5", + "transfer-encoding": "chunked" + }, + "size": 190, + "status": 200 + }, + "server": { + "hostname": "localhost", + "version": "2.5" + }, + "latency": 0 + } +``` + +- **origin**: + +```http + GET /hello?ab=cd HTTP/1.1 + host: localhost + content-length: 6 + connection: close + + abcdef +``` + +## Info + +The `message` will write to the buffer first. +It will send to the rocketmq server when the buffer exceed the `batch_max_size`, +or every `buffer_duration` flush the buffer. + +In case of success, returns `true`. +In case of errors, returns `nil` with a string describing the error (`buffer overflow`). + +### Sample Nameserver list + +Specify the nameservers of the external rocketmq servers as below sample. + +```json +[ + "127.0.0.1:9876", + "127.0.0.2:9876" +] +``` + +## How To Enable + +The following is an example on how to enable the rocketmq-logger for a specific route. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/5 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "batch_max_size": 1, + "name": "rocketmq logger" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" +}' +``` + +## Test Plugin + +success: + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +... +hello, world +``` + +## Metadata + +| Name | Type | Requirement | Default | Valid | Description | +| ---------------- | ------- | ----------- | ------------- | ------- | ---------------------------------------------------------------------------------------- | +| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get `APISIX` variables or [Nginx variable](http://nginx.org/en/docs/varindex.html). | + + Note that **the metadata configuration is applied in global scope**, which means it will take effect on all Route or Service which use rocketmq-logger plugin. + +**APISIX Variables** + +| Variable Name | Description | Usage Example | +|------------------|-------------------------|----------------| +| route_id | id of `route` | $route_id | +| route_name | name of `route` | $route_name | +| service_id | id of `service` | $service_id | +| service_name | name of `service` | $service_name | +| consumer_name | username of `consumer` | $consumer_name | + +### Example + +```shell +curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/rocketmq-logger -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } +}' +``` + +It is expected to see some logs like that: + +```shell +{"host":"localhost","@timestamp":"2020-09-23T19:05:05-04:00","client_ip":"127.0.0.1","route_id":"1"} +{"host":"localhost","@timestamp":"2020-09-23T19:05:05-04:00","client_ip":"127.0.0.1","route_id":"1"} +``` + +## Disable Plugin + +Remove the corresponding json configuration in the plugin configuration to disable the `rocketmq-logger`. +APISIX plugins are hot-reloaded, therefore no need to restart APISIX. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index c8a3a654cafa..d46eb94a851b 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -135,7 +135,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - 高性能:在单核上 QPS 可以达到 18k,同时延迟只有 0.2 毫秒。 - [故障注入](plugins/fault-injection.md) - [REST Admin API](admin-api.md): 使用 REST Admin API 来控制 Apache APISIX,默认只允许 127.0.0.1 访问,你可以修改 `conf/config.yaml` 中的 `allow_admin` 字段,指定允许调用 Admin API 的 IP 列表。同时需要注意的是,Admin API 使用 key auth 来校验调用者身份,**在部署前需要修改 `conf/config.yaml` 中的 `admin_key` 字段,来保证安全。** - - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md), [Google Cloud Logging](plugins/google-cloud-logging.md)) + - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md), [Google Cloud Logging](plugins/google-cloud-logging.md), [RocketMQ Logger](plugins/rocketmq-logger.md)) - [Helm charts](https://github.com/apache/apisix-helm-chart) - **高度可扩展** diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 9a2eef66dd11..cd914b7a4700 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -114,6 +114,7 @@ "plugins/skywalking-logger", "plugins/tcp-logger", "plugins/kafka-logger", + "plugins/rocketmq-logger", "plugins/udp-logger", "plugins/syslog", "plugins/log-rotate", diff --git a/docs/zh/latest/plugins/rocketmq-logger.md b/docs/zh/latest/plugins/rocketmq-logger.md new file mode 100644 index 000000000000..f61c0b4acf9a --- /dev/null +++ b/docs/zh/latest/plugins/rocketmq-logger.md @@ -0,0 +1,229 @@ +--- +title: rocketmq-logger +--- + + + +## 目录 + +- [**简介**](#简介) +- [**属性**](#属性) +- [**工作原理**](#工作原理) +- [**如何启用**](#如何启用) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 简介 + +`rocketmq-logger` 插件可以将接口请求日志以 JSON 的形式推送给外部 rocketmq 集群。 + +如果在短时间内没有收到日志数据,请放心,它会在我们的批处理处理器中的计时器功能到期后自动发送日志。 + +有关 Apache APISIX 中 Batch-Processor 的更多信息,请参考。 +[Batch-Processor](../batch-processor.md) + +## 属性 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ---------------- | ------- | ------ | -------------- | ------- | ------------------------------------------------ | +| nameserver_list | object | 必须 | | | 要推送的 rocketmq 的 nameserver 列表。 | +| topic | string | 必须 | | | 要推送的 topic。 | +| key | string | 可选 | | | 发送消息的keys。 | +| tag | string | 可选 | | | 发送消息的tags。 | +| timeout | integer | 可选 | 3 | [1,...] | 发送数据的超时时间。 | +| use_tls | boolean | 可选 | false | | 是否开启TLS加密。 | +| access_key | string | 可选 | "" | | ACL认证的access key,空字符串表示不开启ACL。 | +| secret_key | string | 可选 | "" | | ACL认证的secret key。 | +| name | string | 可选 | "rocketmq logger" | | batch processor 的唯一标识。 | +| meta_format | enum | 可选 | "default" | ["default","origin"] | `default`:获取请求信息以默认的 JSON 编码方式。`origin`:获取请求信息以 HTTP 原始请求方式。[具体示例](#meta_format-参考示例)| +| batch_max_size | integer | 可选 | 1000 | [1,...] | 设置每批发送日志的最大条数,当日志条数达到设置的最大值时,会自动推送全部日志到 `rocketmq` 服务。| +| inactive_timeout | integer | 可选 | 5 | [1,...] | 刷新缓冲区的最大时间(以秒为单位),当达到最大的刷新时间时,无论缓冲区中的日志数量是否达到设置的最大条数,也会自动将全部日志推送到 `rocketmq` 服务。 | +| buffer_duration | integer | 可选 | 60 | [1,...] | 必须先处理批次中最旧条目的最长期限(以秒为单位)。 | +| max_retry_count | integer | 可选 | 0 | [0,...] | 从处理管道中移除之前的最大重试次数。 | +| retry_delay | integer | 可选 | 1 | [0,...] | 如果执行失败,则应延迟执行流程的秒数。 | +| include_req_body | boolean | 可选 | false | [false, true] | 是否包括请求 body。false: 表示不包含请求的 body ;true: 表示包含请求的 body。注意:如果请求 body 没办法完全放在内存中,由于 Nginx 的限制,我们没有办法把它记录下来。| +| include_req_body_expr | array | 可选 | | | 当 `include_req_body` 开启时, 基于 [lua-resty-expr](https://github.com/api7/lua-resty-expr) 表达式的结果进行记录。如果该选项存在,只有在表达式为真的时候才会记录请求 body。 | +| include_resp_body| boolean | 可选 | false | [false, true] | 是否包括响应体。包含响应体,当为`true`。 | +| include_resp_body_expr | array | 可选 | | | 是否采集响体, 基于[lua-resty-expr](https://github.com/api7/lua-resty-expr)。 该选项需要开启 `include_resp_body`| + +### meta_format 参考示例 + +- **default**: + +```json + { + "upstream": "127.0.0.1:1980", + "start_time": 1619414294760, + "client_ip": "127.0.0.1", + "service_id": "", + "route_id": "1", + "request": { + "querystring": { + "ab": "cd" + }, + "size": 90, + "uri": "/hello?ab=cd", + "url": "http://localhost:1984/hello?ab=cd", + "headers": { + "host": "localhost", + "content-length": "6", + "connection": "close" + }, + "body": "abcdef", + "method": "GET" + }, + "response": { + "headers": { + "connection": "close", + "content-type": "text/plain; charset=utf-8", + "date": "Mon, 26 Apr 2021 05:18:14 GMT", + "server": "APISIX/2.5", + "transfer-encoding": "chunked" + }, + "size": 190, + "status": 200 + }, + "server": { + "hostname": "localhost", + "version": "2.5" + }, + "latency": 0 + } +``` + +- **origin**: + +```http + GET /hello?ab=cd HTTP/1.1 + host: localhost + content-length: 6 + connection: close + + abcdef +``` + +## 工作原理 + +消息将首先写入缓冲区。 +当缓冲区超过 `batch_max_size` 时,它将发送到 rocketmq 服务器, +或每个 `buffer_duration` 刷新缓冲区。 + +如果成功,则返回 `true`。 +如果出现错误,则返回 `nil`,并带有描述错误的字符串(`buffer overflow`)。 + +### Nameserver 列表 + +配置多个nameserver地址如下: + +```json +[ + "127.0.0.1:9876", + "127.0.0.2:9876" +] +``` + +## 如何启用 + +1. 为特定路由启用 rocketmq-logger 插件。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" +}' +``` + +## 测试插件 + + 成功 + +```shell +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 OK +... +hello, world +``` + +## 插件元数据设置 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | +| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 __APISIX__ 变量或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | + +**APISIX 变量** + +| 变量名 | 描述 | 使用示例 | +|------------------|-------------------------|----------------| +| route_id | `route` 的 id | $route_id | +| route_name | `route` 的 name | $route_name | +| service_id | `service` 的 id | $service_id | +| service_name | `service` 的 name | $service_name | +| consumer_name | `consumer` 的 username | $consumer_name | + +### 设置日志格式示例 + +```shell +curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/rocketmq-logger -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } +}' +``` + +在日志收集处,将得到类似下面的日志: + +```shell +{"host":"localhost","@timestamp":"2020-09-23T19:05:05-04:00","client_ip":"127.0.0.1","route_id":"1"} +{"host":"localhost","@timestamp":"2020-09-23T19:05:05-04:00","client_ip":"127.0.0.1","route_id":"1"} +``` + +## 禁用插件 + +当您要禁用 `rocketmq-logger` 插件时,这很简单,您可以在插件配置中删除相应的 json 配置,无需重新启动服务,它将立即生效: + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "methods": ["GET"], + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index 56b1f59ffe50..444471be4919 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -72,6 +72,7 @@ dependencies = { "api7-snowflake = 2.0-1", "inspect == 3.1.1", "lualdap = 1.2.6-1", + "lua-resty-rocketmq = 0.2.1-1", } build = { diff --git a/t/admin/plugins.t b/t/admin/plugins.t index dbe585997b08..4bc6c0d1ee48 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -106,6 +106,7 @@ google-cloud-logging sls-logger tcp-logger kafka-logger +rocketmq-logger syslog udp-logger example-plugin diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 2f38dcbf5151..026569558546 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -81,6 +81,7 @@ loaded plugin and sort by priority: 410 name: http-logger loaded plugin and sort by priority: 406 name: sls-logger loaded plugin and sort by priority: 405 name: tcp-logger loaded plugin and sort by priority: 403 name: kafka-logger +loaded plugin and sort by priority: 402 name: rocketmq-logger loaded plugin and sort by priority: 401 name: syslog loaded plugin and sort by priority: 400 name: udp-logger loaded plugin and sort by priority: 0 name: example-plugin diff --git a/t/plugin/rocketmq-logger-log-format.t b/t/plugin/rocketmq-logger-log-format.t new file mode 100644 index 000000000000..b3a364b14c9c --- /dev/null +++ b/t/plugin/rocketmq-logger-log-format.t @@ -0,0 +1,121 @@ +# +# 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'; + +log_level('info'); +repeat_each(1); +no_long_string(); +no_root_location(); + +run_tests; + +__DATA__ + +=== TEST 1: add plugin metadata +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/rocketmq-logger', + ngx.HTTP_PUT, + [[{ + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } + }]], + [[{ + "node": { + "value": { + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } + } + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 2: set route(id: 1), batch_max_size=1 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "tag" : "tag1", + "timeout" : 1, + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 3: hit route and report rocketmq logger +--- request +GET /hello +--- response_body +hello world +--- wait: 0.5 +--- no_error_log +[error] +--- error_log eval +qr/send data to rocketmq: \{.*"host":"localhost"/ diff --git a/t/plugin/rocketmq-logger.t b/t/plugin/rocketmq-logger.t new file mode 100644 index 000000000000..0ac81229a0cb --- /dev/null +++ b/t/plugin/rocketmq-logger.t @@ -0,0 +1,1098 @@ +# +# 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(); +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + topic = "test", + key = "key1", + nameserver_list = { + "127.0.0.1:3" + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] + + + +=== TEST 2: missing nameserver list +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({topic = "test", key= "key1"}) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +property "nameserver_list" is required +done +--- no_error_log +[error] + + + +=== TEST 3: wrong type of string +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + nameserver_list = { + "127.0.0.1:3000" + }, + timeout = "10", + topic ="test", + key= "key1" + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +property "timeout" validation failed: wrong type: expected integer, got string +done +--- no_error_log +[error] + + + +=== TEST 4: set route(id: 1) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]], + [[{ + "node": { + "value": { + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 5: access +--- request +GET /hello +--- response_body +hello world +--- no_error_log +[error] +--- wait: 2 + + + +=== TEST 6: unavailable nameserver +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9877" ], + "topic" : "test2", + "producer_type": "sync", + "key" : "key1", + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]], + [[{ + "node": { + "value": { + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9877" ], + "topic" : "test2", + "producer_type": "sync", + "key" : "key1", + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local res, err = httpc:request_uri(uri, {method = "GET"}) + } + } +--- request +GET /t +--- error_log +failed to send data to rocketmq topic +[error] +--- wait: 1 + + + +=== TEST 7: set route(meta_format = origin, include_req_body = true) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": true, + "meta_format": "origin" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 8: hit route, report log to rocketmq +--- request +GET /hello?ab=cd +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log +send data to rocketmq: GET /hello?ab=cd HTTP/1.1 +host: localhost +content-length: 6 +connection: close + +abcdef +--- wait: 2 + + + +=== TEST 9: set route(meta_format = origin, include_req_body = false) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false, + "meta_format": "origin" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 10: hit route, report log to rocketmq +--- request +GET /hello?ab=cd +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log +send data to rocketmq: GET /hello?ab=cd HTTP/1.1 +host: localhost +content-length: 6 +connection: close +--- wait: 2 + + + +=== TEST 11: set route(meta_format = default) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 12: hit route, report log to rocketmq +--- request +GET /hello?ab=cd +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log_like eval +qr/send data to rocketmq: \{.*"upstream":"127.0.0.1:1980"/ +--- wait: 2 + + + +=== TEST 13: set route(id: 1), missing key field +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "timeout" : 1, + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]], + [[{ + "node": { + "value": { + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "timeout" : 1, + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }, + "key": "/apisix/routes/1" + }, + "action": "set" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 14: access, test key field is optional +--- request +GET /hello +--- response_body +hello world +--- no_error_log +[error] +--- wait: 2 + + + +=== TEST 15: set route(meta_format = default), missing key field +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 16: hit route, report log to rocketmq +--- request +GET /hello?ab=cd +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log_like eval +qr/send data to rocketmq: \{.*"upstream":"127.0.0.1:1980"/ +--- wait: 2 + + + +=== TEST 17: use the topic with 3 partitions +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test3", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 18: report log to rocketmq by different partitions +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test3", + "producer_type": "sync", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + } + } +--- request +GET /t +--- timeout: 5s +--- ignore_response +--- no_error_log +[error] +--- error_log eval +[qr/queue: 1/, +qr/queue: 0/, +qr/queue: 2/] + + + +=== TEST 19: report log to rocketmq by different partitions in async mode +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test3", + "producer_type": "async", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + } + } +--- request +GET /t +--- timeout: 5s +--- ignore_response +--- no_error_log +[error] +--- error_log eval +[qr/queue: 1/, +qr/queue: 0/, +qr/queue: 2/] + + + +=== TEST 20: update the nameserver_list, generate different rocketmq producers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + ngx.sleep(0.5) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + code, body = t('/apisix/admin/routes/1/plugins', + ngx.HTTP_PATCH, + [[{ + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + code, body = t('/apisix/admin/routes/1/plugins', + ngx.HTTP_PATCH, + [[{ + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:19876" ], + "topic" : "test4", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + ngx.sleep(2) + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 10 +--- response +passed +--- wait: 5 +--- error_log +phase_func(): rocketmq nameserver_list[1] port 9876 +phase_func(): rocketmq nameserver_list[1] port 19876 +--- no_error_log eval +qr/not found topic/ + + + +=== TEST 21: use the topic that does not exist on rocketmq(even if rocketmq allows auto create topics, first time push messages to rocketmq would got this error) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1/plugins', + ngx.HTTP_PATCH, + [[{ + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "undefined_topic", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + ngx.sleep(2) + ngx.say("passed") + } + } +--- request +GET /t +--- timeout: 5 +--- response +passed +--- error_log eval +qr/getTopicRouteInfoFromNameserver return TOPIC_NOT_EXIST, No topic route info in name server for the topic: undefined_topic/ + + + +=== TEST 22: rocketmq nameserver list info in log +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "producer_type": "sync", + "key" : "key1", + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local res, err = httpc:request_uri(uri, {method = "GET"}) + } + } +--- request +GET /t +--- error_log_like eval +qr/create new rocketmq producer instance, nameserver_list: \[\{"port":9876,"host":"127.0.0.127"}]/ +qr/failed to send data to rocketmq topic: .*, nameserver_list: \{"127.0.0.127":9876}/ + + + +=== TEST 23: delete plugin metadata, tests would fail if run rocketmq-logger-log-format.t and plugin metadata is added +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/rocketmq-logger', + ngx.HTTP_DELETE, + nil, + [[{"action": "delete"}]]) + } + } +--- request +GET /t +--- response_body + +--- no_error_log +[error] + + + +=== TEST 24: set route(id: 1,include_req_body = true,include_req_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_req_body": true, + "include_req_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 25: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log eval +qr/send data to rocketmq: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 26: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to rocketmq: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 27: check log schema(include_req_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + topic = "test", + key = "key1", + nameserver_list = { + "127.0.0.1:3" + }, + include_req_body = true, + include_req_body_expr = { + {"bar", "<>", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +failed to validate the 'include_req_body_expr' expression: invalid operator '<>' +done +--- no_error_log +[error] + + + +=== TEST 28: check log schema(include_resp_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + topic = "test", + key = "key1", + nameserver_list = { + "127.0.0.1:3" + }, + include_resp_body = true, + include_resp_body_expr = { + {"bar", "", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +failed to validate the 'include_resp_body_expr' expression: invalid operator '' +done +--- no_error_log +[error] + + + +=== TEST 29: set route(id: 1,include_resp_body = true,include_resp_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_resp_body": true, + "include_resp_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 30: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- no_error_log +[error] +--- error_log eval +qr/send data to rocketmq: \{.*"body":"hello world\\n"/ +--- wait: 2 + + + +=== TEST 31: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to rocketmq: \{.*"body":"hello world\\n"/ +--- wait: 2 From 3d5f554c66903e0dc888303e92d9adf122863dbf Mon Sep 17 00:00:00 2001 From: Bo Shao Date: Tue, 7 Dec 2021 11:40:47 +0800 Subject: [PATCH 165/260] docs: update README.md (#5713) --- README.md | 6 +++--- docs/zh/latest/README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b3c8909bcc00..02febfbaccdd 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,10 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - Custom routing: Support users to implement routing algorithms themselves. - **Multi-Language support** - - Apache APISIX is a multi-language gateway for plugin development and provides support via `WASM` and `RPC`. + - Apache APISIX is a multi-language gateway for plugin development and provides support via `RPC` and `WASM`. ![Multi Language Support into Apache APISIX](docs/assets/images/apisix-multi-lang-support.png) - - The WASM or WebAssembly, is the modern way. APISIX can load and run WASM bytecode via APISIX [wasm plugin](https://github.com/apache/apisix/blob/master/docs/en/latest/wasm.md) written with the [Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks). Developers only need to write the code according to the SDK and then compile it into a WASM bytecode that runs on WASM VM with APISIX. - - The RPC way, is a traditional way. Developers can choose the language according to their needs and after starting an independent process with the RPC, it exchanges data with APISIX through local RPC communication. Till this moment, APISIX has support for [Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) and Node.js. + - The RPC way, is the current way. Developers can choose the language according to their needs and after starting an independent process with the RPC, it exchanges data with APISIX through local RPC communication. Till this moment, APISIX has support for [Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) and Node.js. + - The WASM or WebAssembly, is an experimental way. APISIX can load and run WASM bytecode via APISIX [wasm plugin](https://github.com/apache/apisix/blob/master/docs/en/latest/wasm.md) written with the [Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks). Developers only need to write the code according to the SDK and then compile it into a WASM bytecode that runs on WASM VM with APISIX. - **Serverless** - [Lua functions](docs/en/latest/plugins/serverless.md): Invoke functions in each phase in APISIX. diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index d46eb94a851b..d6ed28d0b52d 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -145,10 +145,10 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - 自定义路由: 支持用户自己实现路由算法。 - **多语言支持** - - Apache APISIX 是一个通过 `WASM` 和 `RPC` 支持不同语言来进行插件开发的网关. +- Apache APISIX 是一个通过 `RPC` 和 `WASM` 支持不同语言来进行插件开发的网关. ![Multi Language Support into Apache APISIX](../../../docs/assets/images/apisix-multi-lang-support.png) - - WASM 或 WebAssembly 是比较现代的开发方式。 APISIX 能加载运行使用[Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks)编译的 WASM 字节码。开发者仅需要使用该 SDK 编写代码,然后编译成 WASM 字节码,即可运行在 APISIX 中的 WASM 虚拟机中。 - - RPC 是一种比较传统的开发方式。开发者可以使用他们需要的语言来进行 RPC 服务的开发,该 RPC 通过本地通讯来跟 APISIX 进行数据交换。到目前为止,APISIX 已支持[Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) and Node.js. + - RPC 是当前采用的开发方式。开发者可以使用他们需要的语言来进行 RPC 服务的开发,该 RPC 通过本地通讯来跟 APISIX 进行数据交换。到目前为止,APISIX 已支持[Java](https://github.com/apache/apisix-java-plugin-runner), [Golang](https://github.com/apache/apisix-go-plugin-runner), [Python](https://github.com/apache/apisix-python-plugin-runner) 和 Node.js。 + - WASM 或 WebAssembly 是实验性的开发方式。 APISIX 能加载运行使用[Proxy WASM SDK](https://github.com/proxy-wasm/spec#sdks)编译的 WASM 字节码。开发者仅需要使用该 SDK 编写代码,然后编译成 WASM 字节码,即可运行在 APISIX 中的 WASM 虚拟机中。 - **Serverless** - [Lua functions](plugins/serverless.md): 能在 APISIX 每个阶段调用 lua 函数. From a4b6931390c302724675f9026078d479c6295119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 7 Dec 2021 15:35:59 +0800 Subject: [PATCH 166/260] feat(wasm): allow running in the rewrite phase (#5695) --- apisix/cli/ops.lua | 243 +----------------------- apisix/cli/schema.lua | 274 ++++++++++++++++++++++++++++ apisix/core/config_local.lua | 4 + apisix/discovery/consul_kv/init.lua | 3 - apisix/discovery/dns/init.lua | 4 - apisix/discovery/eureka/init.lua | 4 - apisix/discovery/nacos/init.lua | 4 - apisix/init.lua | 20 +- apisix/wasm.lua | 14 +- docs/en/latest/wasm.md | 3 +- t/wasm/route.t | 35 +++- 11 files changed, 333 insertions(+), 275 deletions(-) create mode 100644 apisix/cli/schema.lua diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 3f28e8fedaa7..4db2adf4e378 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -18,19 +18,17 @@ local ver = require("apisix.core.version") local etcd = require("apisix.cli.etcd") local util = require("apisix.cli.util") local file = require("apisix.cli.file") +local schema = require("apisix.cli.schema") local ngx_tpl = require("apisix.cli.ngx_tpl") local profile = require("apisix.core.profile") local template = require("resty.template") local argparse = require("argparse") local pl_path = require("pl.path") -local jsonschema = require("jsonschema") local stderr = io.stderr local ipairs = ipairs local pairs = pairs -local pcall = pcall local print = print -local require = require local type = type local tostring = tostring local tonumber = tonumber @@ -148,227 +146,6 @@ local function get_lua_path(conf) end -local config_schema = { - type = "object", - properties = { - apisix = { - properties = { - config_center = { - enum = {"etcd", "yaml"}, - }, - lua_module_hook = { - pattern = "^[a-zA-Z._-]+$", - }, - proxy_protocol = { - type = "object", - properties = { - listen_http_port = { - type = "integer", - }, - listen_https_port = { - type = "integer", - }, - enable_tcp_pp = { - type = "boolean", - }, - enable_tcp_pp_to_upstream = { - type = "boolean", - }, - } - }, - proxy_cache = { - type = "object", - properties = { - zones = { - type = "array", - minItems = 1, - items = { - type = "object", - properties = { - name = { - type = "string", - }, - memory_size = { - type = "string", - }, - disk_size = { - type = "string", - }, - disk_path = { - type = "string", - }, - cache_levels = { - type = "string", - }, - }, - oneOf = { - { - required = {"name", "memory_size"}, - maxProperties = 2, - }, - { - required = {"name", "memory_size", "disk_size", - "disk_path", "cache_levels"}, - } - }, - }, - uniqueItems = true, - } - } - }, - port_admin = { - type = "integer", - }, - https_admin = { - type = "boolean", - }, - stream_proxy = { - type = "object", - properties = { - tcp = { - type = "array", - minItems = 1, - items = { - anyOf = { - { - type = "integer", - }, - { - type = "string", - }, - { - type = "object", - properties = { - addr = { - anyOf = { - { - type = "integer", - }, - { - type = "string", - }, - } - }, - tls = { - type = "boolean", - } - }, - required = {"addr"} - }, - }, - }, - uniqueItems = true, - }, - udp = { - type = "array", - minItems = 1, - items = { - anyOf = { - { - type = "integer", - }, - { - type = "string", - }, - }, - }, - uniqueItems = true, - }, - } - }, - dns_resolver = { - type = "array", - minItems = 1, - items = { - type = "string", - } - }, - dns_resolver_valid = { - type = "integer", - }, - ssl = { - type = "object", - properties = { - ssl_trusted_certificate = { - type = "string", - } - } - }, - } - }, - nginx_config = { - type = "object", - properties = { - envs = { - type = "array", - minItems = 1, - items = { - type = "string", - } - } - }, - }, - http = { - type = "object", - properties = { - custom_lua_shared_dict = { - type = "object", - } - } - }, - etcd = { - type = "object", - properties = { - resync_delay = { - type = "integer", - }, - user = { - type = "string", - }, - password = { - type = "string", - }, - tls = { - type = "object", - properties = { - cert = { - type = "string", - }, - key = { - type = "string", - }, - } - } - } - }, - wasm = { - type = "object", - properties = { - plugins = { - type = "array", - minItems = 1, - items = { - type = "object", - properties = { - name = { - type = "string" - }, - file = { - type = "string" - }, - priority = { - type = "integer" - } - }, - required = {"name", "file", "priority"} - } - } - } - }, - } -} - - local function init(env) if env.is_root_path then print('Warning! Running apisix under /root is only suitable for ' @@ -391,23 +168,9 @@ local function init(env) util.die("failed to read local yaml config of apisix: ", err, "\n") end - local validator = jsonschema.generate_validator(config_schema) - local ok, err = validator(yaml_conf) + local ok, err = schema.validate(yaml_conf) if not ok then - util.die("failed to validate config: ", err, "\n") - end - - if yaml_conf.discovery then - for kind, conf in pairs(yaml_conf.discovery) do - local ok, schema = pcall(require, "apisix.discovery." .. kind .. ".schema") - if ok then - local validator = jsonschema.generate_validator(schema) - local ok, err = validator(conf) - if not ok then - util.die("invalid discovery ", kind, " configuration: ", err, "\n") - end - end - end + util.die(err, "\n") end -- check the Admin API token diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua new file mode 100644 index 000000000000..e479074560e0 --- /dev/null +++ b/apisix/cli/schema.lua @@ -0,0 +1,274 @@ +-- +-- 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 jsonschema = require("jsonschema") +local pairs = pairs +local pcall = pcall +local require = require + + +local _M = {} +local config_schema = { + type = "object", + properties = { + apisix = { + properties = { + config_center = { + enum = {"etcd", "yaml"}, + }, + lua_module_hook = { + pattern = "^[a-zA-Z._-]+$", + }, + proxy_protocol = { + type = "object", + properties = { + listen_http_port = { + type = "integer", + }, + listen_https_port = { + type = "integer", + }, + enable_tcp_pp = { + type = "boolean", + }, + enable_tcp_pp_to_upstream = { + type = "boolean", + }, + } + }, + proxy_cache = { + type = "object", + properties = { + zones = { + type = "array", + minItems = 1, + items = { + type = "object", + properties = { + name = { + type = "string", + }, + memory_size = { + type = "string", + }, + disk_size = { + type = "string", + }, + disk_path = { + type = "string", + }, + cache_levels = { + type = "string", + }, + }, + oneOf = { + { + required = {"name", "memory_size"}, + maxProperties = 2, + }, + { + required = {"name", "memory_size", "disk_size", + "disk_path", "cache_levels"}, + } + }, + }, + uniqueItems = true, + } + } + }, + port_admin = { + type = "integer", + }, + https_admin = { + type = "boolean", + }, + stream_proxy = { + type = "object", + properties = { + tcp = { + type = "array", + minItems = 1, + items = { + anyOf = { + { + type = "integer", + }, + { + type = "string", + }, + { + type = "object", + properties = { + addr = { + anyOf = { + { + type = "integer", + }, + { + type = "string", + }, + } + }, + tls = { + type = "boolean", + } + }, + required = {"addr"} + }, + }, + }, + uniqueItems = true, + }, + udp = { + type = "array", + minItems = 1, + items = { + anyOf = { + { + type = "integer", + }, + { + type = "string", + }, + }, + }, + uniqueItems = true, + }, + } + }, + dns_resolver = { + type = "array", + minItems = 1, + items = { + type = "string", + } + }, + dns_resolver_valid = { + type = "integer", + }, + ssl = { + type = "object", + properties = { + ssl_trusted_certificate = { + type = "string", + } + } + }, + } + }, + nginx_config = { + type = "object", + properties = { + envs = { + type = "array", + minItems = 1, + items = { + type = "string", + } + } + }, + }, + http = { + type = "object", + properties = { + custom_lua_shared_dict = { + type = "object", + } + } + }, + etcd = { + type = "object", + properties = { + resync_delay = { + type = "integer", + }, + user = { + type = "string", + }, + password = { + type = "string", + }, + tls = { + type = "object", + properties = { + cert = { + type = "string", + }, + key = { + type = "string", + }, + } + } + } + }, + wasm = { + type = "object", + properties = { + plugins = { + type = "array", + minItems = 1, + items = { + type = "object", + properties = { + name = { + type = "string" + }, + file = { + type = "string" + }, + priority = { + type = "integer" + }, + http_request_phase = { + enum = {"access", "rewrite"}, + default = "access", + }, + }, + required = {"name", "file", "priority"} + } + } + } + }, + } +} + + +function _M.validate(yaml_conf) + local validator = jsonschema.generate_validator(config_schema) + local ok, err = validator(yaml_conf) + if not ok then + return false, "failed to validate config: " .. err + end + + if yaml_conf.discovery then + for kind, conf in pairs(yaml_conf.discovery) do + local ok, schema = pcall(require, "apisix.discovery." .. kind .. ".schema") + if ok then + local validator = jsonschema.generate_validator(schema) + local ok, err = validator(conf) + if not ok then + return false, "invalid discovery " .. kind .. " configuration: " .. err + end + end + end + end + + return true +end + + +return _M diff --git a/apisix/core/config_local.lua b/apisix/core/config_local.lua index d17255b7d8b6..9494dad020a9 100644 --- a/apisix/core/config_local.lua +++ b/apisix/core/config_local.lua @@ -16,6 +16,7 @@ -- local file = require("apisix.cli.file") +local schema = require("apisix.cli.schema") local _M = {} @@ -39,6 +40,9 @@ function _M.local_conf(force) return nil, err end + -- fill the default value by the schema + schema.validate(default_conf) + config_data = default_conf return config_data end diff --git a/apisix/discovery/consul_kv/init.lua b/apisix/discovery/consul_kv/init.lua index 71b72c8d97ea..fbb2061dba25 100644 --- a/apisix/discovery/consul_kv/init.lua +++ b/apisix/discovery/consul_kv/init.lua @@ -18,7 +18,6 @@ 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 schema = require('apisix.discovery.consul_kv.schema') local resty_consul = require('resty.consul') local cjson = require('cjson') local http = require('resty.http') @@ -364,8 +363,6 @@ end function _M.init_worker() local consul_conf = local_conf.discovery.consul_kv - -- inject the default values - core.schema.check(schema, consul_conf) if consul_conf.dump then local dump = consul_conf.dump diff --git a/apisix/discovery/dns/init.lua b/apisix/discovery/dns/init.lua index 45a894080fa5..837f1e8e8ca0 100644 --- a/apisix/discovery/dns/init.lua +++ b/apisix/discovery/dns/init.lua @@ -17,7 +17,6 @@ local core = require("apisix.core") local config_local = require("apisix.core.config_local") -local schema = require('apisix.discovery.dns.schema') local ipairs = ipairs local error = error @@ -52,9 +51,6 @@ end function _M.init_worker() local local_conf = config_local.local_conf() - -- inject the default values - core.schema.check(schema, local_conf.discovery.dns) - local servers = local_conf.discovery.dns.servers local opts = { diff --git a/apisix/discovery/eureka/init.lua b/apisix/discovery/eureka/init.lua index 79faf9eea571..df72a5269e59 100644 --- a/apisix/discovery/eureka/init.lua +++ b/apisix/discovery/eureka/init.lua @@ -18,7 +18,6 @@ local local_conf = require("apisix.core.config_local").local_conf() local http = require("resty.http") local core = require("apisix.core") -local schema = require('apisix.discovery.eureka.schema') local ipmatcher = require("resty.ipmatcher") local ipairs = ipairs local tostring = tostring @@ -207,9 +206,6 @@ end function _M.init_worker() - -- inject the default values - core.schema.check(schema, local_conf.discovery.eureka) - default_weight = local_conf.discovery.eureka.weight or 100 log.info("default_weight:", default_weight, ".") local fetch_interval = local_conf.discovery.eureka.fetch_interval or 30 diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua index 94a44a2aead7..d163b3952cd2 100644 --- a/apisix/discovery/nacos/init.lua +++ b/apisix/discovery/nacos/init.lua @@ -19,7 +19,6 @@ local require = require local local_conf = require('apisix.core.config_local').local_conf() local http = require('resty.http') local core = require('apisix.core') -local schema = require('apisix.discovery.nacos.schema') local ipairs = ipairs local type = type local math = math @@ -333,9 +332,6 @@ end function _M.init_worker() - -- inject the default values - core.schema.check(schema, local_conf.discovery.nacos) - events = require("resty.worker.events") events_list = events.event_list("discovery_nacos_update_application", "updating") diff --git a/apisix/init.lua b/apisix/init.lua index 801809f64b10..f5ef0c4c651a 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -15,6 +15,16 @@ -- limitations under the License. -- local require = require +-- set the JIT options before any code, to prevent error "changing jit stack size is not +-- allowed when some regexs have already been compiled and cached" +if require("ffi").os == "Linux" then + require("ngx.re").opt("jit_stack_size", 200 * 1024) +end + +require("jit.opt").start("minstitch=2", "maxtrace=4000", + "maxrecord=8000", "sizemcode=64", + "maxmcode=4000", "maxirconst=1000") + require("apisix.patch").patch() local core = require("apisix.core") local plugin = require("apisix.plugin") @@ -61,16 +71,6 @@ local _M = {version = 0.4} function _M.http_init(args) - require("resty.core") - - if require("ffi").os == "Linux" then - require("ngx.re").opt("jit_stack_size", 200 * 1024) - end - - require("jit.opt").start("minstitch=2", "maxtrace=4000", - "maxrecord=8000", "sizemcode=64", - "maxmcode=4000", "maxirconst=1000") - core.resolver.init_resolver(args) core.id.init() diff --git a/apisix/wasm.lua b/apisix/wasm.lua index 939549a2ad9f..7a6e81c4b813 100644 --- a/apisix/wasm.lua +++ b/apisix/wasm.lua @@ -62,7 +62,7 @@ local function fetch_plugin_ctx(conf, ctx, plugin) end -local function access_wrapper(self, conf, ctx) +local function http_request_wrapper(self, conf, ctx) local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin) if not plugin_ctx then core.log.error("failed to fetch wasm plugin ctx: ", err) @@ -113,9 +113,17 @@ function _M.require(attrs) plugin = plugin, type = "wasm", } - mod.access = function (conf, ctx) - return access_wrapper(mod, conf, ctx) + + if attrs.http_request_phase == "rewrite" then + mod.rewrite = function (conf, ctx) + return http_request_wrapper(mod, conf, ctx) + end + else + mod.access = function (conf, ctx) + return http_request_wrapper(mod, conf, ctx) + end end + mod.header_filter = function (conf, ctx) return header_filter_wrapper(mod, conf, ctx) end diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md index 8d81d69a2e00..31e9ca4fac00 100644 --- a/docs/en/latest/wasm.md +++ b/docs/en/latest/wasm.md @@ -65,6 +65,7 @@ wasm: - name: wasm_log # the name of the plugin priority: 7999 # priority file: t/wasm/log/main.go.wasm # the path of `.wasm` file + http_request_phase: access # default to "access", can be one of ["access", "rewrite"] ``` That's all. Now you can use the wasm plugin as a regular plugin. @@ -99,5 +100,5 @@ Here is the mapping between Proxy WASM callbacks and APISIX's phases: * `proxy_on_configure`: run once there is not PluginContext for the new configuration. For example, when the first request hits the route which has WASM plugin configured. -* `proxy_on_http_request_headers`: run in the access phase. +* `proxy_on_http_request_headers`: run in the access/rewrite phase, depends on the configuration of `http_request_phase`. * `proxy_on_http_response_headers`: run in the header_filter phase. diff --git a/t/wasm/route.t b/t/wasm/route.t index cb348011c1a7..79afcfc42118 100644 --- a/t/wasm/route.t +++ b/t/wasm/route.t @@ -36,7 +36,8 @@ add_block_preprocessor(sub { $block->set_value("request", "GET /t"); } - my $extra_yaml_config = <<_EOC_; + if (!defined $block->extra_yaml_config) { + my $extra_yaml_config = <<_EOC_; wasm: plugins: - name: wasm_log @@ -46,7 +47,8 @@ wasm: priority: 7998 file: t/wasm/log/main.go.wasm _EOC_ - $block->set_value("extra_yaml_config", $extra_yaml_config); + $block->set_value("extra_yaml_config", $extra_yaml_config); + } }); run_tests(); @@ -145,7 +147,28 @@ run plugin ctx 1 with conf zzz in http ctx 2 -=== TEST 4: plugin from service +=== TEST 4: run wasm plugin in rewrite phase (prior to the one run in access phase) +--- extra_yaml_config +wasm: + plugins: + - name: wasm_log + priority: 7999 + file: t/wasm/log/main.go.wasm + - name: wasm_log2 + priority: 7998 + file: t/wasm/log/main.go.wasm + http_request_phase: rewrite +--- request +GET /hello +--- grep_error_log eval +qr/run plugin ctx \d+ with conf \S+ in http ctx \d+/ +--- grep_error_log_out +run plugin ctx 1 with conf zzz in http ctx 2 +run plugin ctx 1 with conf blahblah in http ctx 2 + + + +=== TEST 5: plugin from service --- config location /t { content_by_lua_block { @@ -212,7 +235,7 @@ passed -=== TEST 5: hit +=== TEST 6: hit --- config location /t { content_by_lua_block { @@ -243,7 +266,7 @@ run plugin ctx 3 with conf blahblah in http ctx 4 -=== TEST 6: plugin from plugin_config +=== TEST 7: plugin from plugin_config --- config location /t { content_by_lua_block { @@ -316,7 +339,7 @@ passed -=== TEST 7: hit +=== TEST 8: hit --- config location /t { content_by_lua_block { From b85ebd4e1cc1a41f7799f6458d421d56169e5e05 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Tue, 7 Dec 2021 17:22:07 +0800 Subject: [PATCH 167/260] fix(patch): add global `math.randomseed` patch support (#5682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- apisix/patch.lua | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/apisix/patch.lua b/apisix/patch.lua index 51cb14bff8e9..69506e6594bb 100644 --- a/apisix/patch.lua +++ b/apisix/patch.lua @@ -20,18 +20,23 @@ local ipmatcher = require("resty.ipmatcher") local socket = require("socket") local unix_socket = require("socket.unix") local ssl = require("ssl") +local ngx = ngx local get_phase = ngx.get_phase local ngx_socket = ngx.socket local original_tcp = ngx.socket.tcp local original_udp = ngx.socket.udp local concat_tab = table.concat +local debug = debug local new_tab = require("table.new") local log = ngx.log local WARN = ngx.WARN local ipairs = ipairs local select = select local setmetatable = setmetatable +local string = string +local table = table local type = type +local tonumber = tonumber local config_local @@ -86,6 +91,48 @@ do end +do -- `math.randomseed` patch + -- `math.random` generates PRND(pseudo-random numbers) from the seed set by `math.randomseed` + -- Many module libraries use `ngx.time` and `ngx.worker.pid` to generate seeds which may + -- loss randomness in container env (where pids are identical, e.g. root pid is 1) + -- Kubernetes may launch multi instance with deployment RS at the same time, `ngx.time` may + -- get same return in the pods. + -- Therefore, this global patch enforce entire framework to use + -- the best-practice PRND generates. + + local resty_random = require("resty.random") + local math_randomseed = math.randomseed + local seeded = {} + + -- make linter happy + -- luacheck: ignore + math.randomseed = function() + local worker_pid = ngx.worker.pid() + + -- check seed mark + if seeded[worker_pid] then + log(ngx.DEBUG, debug.traceback("Random seed has been inited", 2)) + return + end + + -- generate randomseed + -- chose 6 from APISIX's SIX, 256 ^ 6 should do the trick + -- it shouldn't be large than 16 to prevent overflow. + local random_bytes = resty_random.bytes(6) + local t = {} + + for i = 1, #random_bytes do + t[i] = string.byte(random_bytes, i) + end + + local s = table.concat(t) + + math_randomseed(tonumber(s)) + seeded[worker_pid] = true + end +end -- do + + local patch_udp_socket do local old_udp_sock_setpeername From 5f5021649ead293eaad41ccddc95fa9d3f5006b7 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Tue, 7 Dec 2021 18:44:31 -0600 Subject: [PATCH 168/260] chore: delete expired inspiration statements (#5716) --- apisix/core/response.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/apisix/core/response.lua b/apisix/core/response.lua index 9ce9c400daad..221c18429ce1 100644 --- a/apisix/core/response.lua +++ b/apisix/core/response.lua @@ -164,8 +164,6 @@ end -- final_body = transform(final_body) -- ngx.arg[1] = final_body -- ... --- --- Inspired by kong.response.get_raw_body() function _M.hold_body_chunk(ctx, hold_the_copy) local body_buffer local chunk, eof = arg[1], arg[2] From bc091ad433be7bf7b13fae3e9356e529657cb0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 8 Dec 2021 09:11:46 +0800 Subject: [PATCH 169/260] test(kafka-logger): reduce duplicate sections (#5717) --- t/plugin/kafka-logger.t | 689 +-------------------------------------- t/plugin/kafka-logger2.t | 611 ++++++++++++++++++++++++++++++++++ 2 files changed, 624 insertions(+), 676 deletions(-) create mode 100644 t/plugin/kafka-logger2.t diff --git a/t/plugin/kafka-logger.t b/t/plugin/kafka-logger.t index 42277c6f1301..0cc68fe10b04 100644 --- a/t/plugin/kafka-logger.t +++ b/t/plugin/kafka-logger.t @@ -19,6 +19,19 @@ use t::APISIX 'no_plan'; repeat_each(1); no_long_string(); no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + run_tests; __DATA__ @@ -41,12 +54,8 @@ __DATA__ ngx.say("done") } } ---- request -GET /t --- response_body done ---- no_error_log -[error] @@ -62,13 +71,9 @@ done ngx.say("done") } } ---- request -GET /t --- response_body property "broker_list" is required done ---- no_error_log -[error] @@ -91,13 +96,9 @@ done ngx.say("done") } } ---- request -GET /t --- response_body property "timeout" validation failed: wrong type: expected integer, got string done ---- no_error_log -[error] @@ -163,12 +164,8 @@ done ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -177,8 +174,6 @@ passed GET /hello --- response_body hello world ---- no_error_log -[error] --- wait: 2 @@ -251,8 +246,6 @@ hello world local res, err = httpc:request_uri(uri, {method = "GET"}) } } ---- request -GET /t --- error_log failed to send data to Kafka topic [error] @@ -296,12 +289,8 @@ failed to send data to Kafka topic ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -311,8 +300,6 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] --- error_log send data to kafka: GET /hello?ab=cd HTTP/1.1 host: localhost @@ -360,12 +347,8 @@ abcdef ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -375,8 +358,6 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] --- error_log send data to kafka: GET /hello?ab=cd HTTP/1.1 host: localhost @@ -421,12 +402,8 @@ connection: close ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -436,8 +413,6 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] --- error_log_like eval qr/send data to kafka: \{.*"upstream":"127.0.0.1:1980"/ --- wait: 2 @@ -504,12 +479,8 @@ qr/send data to kafka: \{.*"upstream":"127.0.0.1:1980"/ ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -518,8 +489,6 @@ passed GET /hello --- response_body hello world ---- no_error_log -[error] --- wait: 2 @@ -558,12 +527,8 @@ hello world ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -573,8 +538,6 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] --- error_log_like eval qr/send data to kafka: \{.*"upstream":"127.0.0.1:1980"/ --- wait: 2 @@ -615,12 +578,8 @@ qr/send data to kafka: \{.*"upstream":"127.0.0.1:1980"/ ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -662,12 +621,8 @@ passed ngx.sleep(0.5) } } ---- request -GET /t --- timeout: 5s --- ignore_response ---- no_error_log -[error] --- error_log eval [qr/partition_id: 1/, qr/partition_id: 0/, @@ -712,627 +667,9 @@ qr/partition_id: 2/] ngx.sleep(0.5) } } ---- request -GET /t --- timeout: 5s --- ignore_response ---- no_error_log -[error] --- error_log eval [qr/partition_id: 1/, qr/partition_id: 0/, qr/partition_id: 2/] - - - -=== TEST 20: required_acks, matches none of the enum values ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.kafka-logger") - local ok, err = plugin.check_schema({ - broker_list = { - ["127.0.0.1"] = 3000 - }, - required_acks = 10, - kafka_topic ="test", - key= "key1" - }) - if not ok then - ngx.say(err) - end - ngx.say("done") - } - } ---- request -GET /t ---- response_body -property "required_acks" validation failed: matches none of the enum values -done ---- no_error_log -[error] - - - -=== TEST 21: report log to kafka, with required_acks(1, 0, -1) ---- config -location /t { - content_by_lua_block { - local data = { - { - input = { - plugins = { - ["kafka-logger"] = { - broker_list = { - ["127.0.0.1"] = 9092 - }, - kafka_topic = "test2", - producer_type = "sync", - timeout = 1, - batch_max_size = 1, - required_acks = 1, - meta_format = "origin", - } - }, - upstream = { - nodes = { - ["127.0.0.1:1980"] = 1 - }, - type = "roundrobin" - }, - uri = "/hello", - }, - }, - { - input = { - plugins = { - ["kafka-logger"] = { - broker_list = { - ["127.0.0.1"] = 9092 - }, - kafka_topic = "test2", - producer_type = "sync", - timeout = 1, - batch_max_size = 1, - required_acks = -1, - meta_format = "origin", - } - }, - upstream = { - nodes = { - ["127.0.0.1:1980"] = 1 - }, - type = "roundrobin" - }, - uri = "/hello", - }, - }, - { - input = { - plugins = { - ["kafka-logger"] = { - broker_list = { - ["127.0.0.1"] = 9092 - }, - kafka_topic = "test2", - producer_type = "sync", - timeout = 1, - batch_max_size = 1, - required_acks = 0, - meta_format = "origin", - } - }, - upstream = { - nodes = { - ["127.0.0.1:1980"] = 1 - }, - type = "roundrobin" - }, - uri = "/hello", - }, - }, - } - - local t = require("lib.test_admin").test - local err_count = 0 - for i in ipairs(data) do - local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[i].input) - - if code >= 300 then - err_count = err_count + 1 - end - ngx.print(body) - - t('/hello', ngx.HTTP_GET) - end - - assert(err_count == 0) - } -} ---- request -GET /t ---- no_error_log -[error] ---- error_log -send data to kafka: GET /hello -send data to kafka: GET /hello -send data to kafka: GET /hello - - - -=== TEST 22: update the broker_list and cluster_name, generate different kafka producers ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]] - ) - ngx.sleep(0.5) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - code, body = t('/apisix/admin/global_rules/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "kafka-logger": { - "broker_list" : { - "127.0.0.1": 9092 - }, - "kafka_topic" : "test2", - "timeout" : 1, - "batch_max_size": 1, - "include_req_body": false, - "cluster_name": 1 - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - t('/hello',ngx.HTTP_GET) - ngx.sleep(0.5) - - code, body = t('/apisix/admin/global_rules/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "kafka-logger": { - "broker_list" : { - "127.0.0.1": 19092 - }, - "kafka_topic" : "test4", - "timeout" : 1, - "batch_max_size": 1, - "include_req_body": false, - "cluster_name": 2 - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - t('/hello',ngx.HTTP_GET) - ngx.sleep(0.5) - - ngx.sleep(2) - ngx.say("passed") - } - } ---- request -GET /t ---- timeout: 10 ---- response -passed ---- wait: 5 ---- error_log -phase_func(): kafka cluster name 1, broker_list[1] port 9092 -phase_func(): kafka cluster name 2, broker_list[1] port 19092 ---- no_error_log eval -qr/not found topic/ - - - -=== TEST 23: use the topic that does not exist on kafka(even if kafka allows auto create topics, first time push messages to kafka would got this error) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/global_rules/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "kafka-logger": { - "broker_list" : { - "127.0.0.1": 9092 - }, - "kafka_topic" : "undefined_topic", - "timeout" : 1, - "batch_max_size": 1, - "include_req_body": false - } - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - t('/hello',ngx.HTTP_GET) - ngx.sleep(0.5) - - ngx.sleep(2) - ngx.say("passed") - } - } ---- request -GET /t ---- timeout: 5 ---- response -passed ---- error_log eval -qr/not found topic, retryable: true, topic: undefined_topic, partition_id: -1/ - - - -=== TEST 24: check broker_list via schema ---- config - location /t { - content_by_lua_block { - local data = { - { - input = { - broker_list = {}, - kafka_topic = "test", - key= "key1", - }, - }, - { - input = { - broker_list = { - ["127.0.0.1"] = "9092" - }, - kafka_topic = "test", - key= "key1", - }, - }, - { - input = { - broker_list = { - ["127.0.0.1"] = 0 - }, - kafka_topic = "test", - key= "key1", - }, - }, - { - input = { - broker_list = { - ["127.0.0.1"] = 65536 - }, - kafka_topic = "test", - key= "key1", - }, - }, - } - - local plugin = require("apisix.plugins.kafka-logger") - - local err_count = 0 - for i in ipairs(data) do - local ok, err = plugin.check_schema(data[i].input) - if not ok then - err_count = err_count + 1 - ngx.say(err) - end - end - - assert(err_count == #data) - } - } ---- request -GET /t ---- response_body -property "broker_list" validation failed: expect object to have at least 1 properties -property "broker_list" validation failed: failed to validate 127.0.0.1 (matching ".*"): wrong type: expected integer, got string -property "broker_list" validation failed: failed to validate 127.0.0.1 (matching ".*"): expected 0 to be greater than 1 -property "broker_list" validation failed: failed to validate 127.0.0.1 (matching ".*"): expected 65536 to be smaller than 65535 ---- no_error_log -[error] - - - -=== TEST 25: kafka brokers info in log ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "kafka-logger": { - "broker_list" : - { - "127.0.0.127":9092 - }, - "kafka_topic" : "test2", - "producer_type": "sync", - "key" : "key1", - "batch_max_size": 1, - "cluster_name": 10 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" - local res, err = httpc:request_uri(uri, {method = "GET"}) - } - } ---- request -GET /t ---- error_log_like eval -qr/create new kafka producer instance, brokers: \[\{"port":9092,"host":"127.0.0.127"}]/ -qr/failed to send data to Kafka topic: .*, brokers: \{"127.0.0.127":9092}/ - - - -=== TEST 26: set route(id: 1,include_req_body = true,include_req_body_expr = array) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [=[{ - "plugins": { - "kafka-logger": { - "broker_list" : - { - "127.0.0.1":9092 - }, - "kafka_topic" : "test2", - "key" : "key1", - "timeout" : 1, - "include_req_body": true, - "include_req_body_expr": [ - [ - "arg_name", - "==", - "qwerty" - ] - ], - "batch_max_size": 1 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]=] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } - ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 27: hit route, expr eval success ---- request -POST /hello?name=qwerty -abcdef ---- response_body -hello world ---- no_error_log -[error] ---- error_log eval -qr/send data to kafka: \{.*"body":"abcdef"/ ---- wait: 2 - - - -=== TEST 28: hit route,expr eval fail ---- request -POST /hello?name=zcxv -abcdef ---- response_body -hello world ---- no_error_log eval -qr/send data to kafka: \{.*"body":"abcdef"/ ---- wait: 2 - - - -=== TEST 29: check log schema(include_req_body) ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.kafka-logger") - local ok, err = plugin.check_schema({ - kafka_topic = "test", - key = "key1", - broker_list = { - ["127.0.0.1"] = 3 - }, - include_req_body = true, - include_req_body_expr = { - {"bar", "<>", "foo"} - } - }) - if not ok then - ngx.say(err) - end - ngx.say("done") - } - } ---- request -GET /t ---- response_body -failed to validate the 'include_req_body_expr' expression: invalid operator '<>' -done ---- no_error_log -[error] - - - -=== TEST 30: check log schema(include_resp_body) ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.kafka-logger") - local ok, err = plugin.check_schema({ - kafka_topic = "test", - key = "key1", - broker_list = { - ["127.0.0.1"] = 3 - }, - include_resp_body = true, - include_resp_body_expr = { - {"bar", "", "foo"} - } - }) - if not ok then - ngx.say(err) - end - ngx.say("done") - } - } ---- request -GET /t ---- response_body -failed to validate the 'include_resp_body_expr' expression: invalid operator '' -done ---- no_error_log -[error] - - - -=== TEST 31: set route(id: 1,include_resp_body = true,include_resp_body_expr = array) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [=[{ - "plugins": { - "kafka-logger": { - "broker_list" : - { - "127.0.0.1":9092 - }, - "kafka_topic" : "test2", - "key" : "key1", - "timeout" : 1, - "include_resp_body": true, - "include_resp_body_expr": [ - [ - "arg_name", - "==", - "qwerty" - ] - ], - "batch_max_size": 1 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]=] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } - ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 32: hit route, expr eval success ---- request -POST /hello?name=qwerty -abcdef ---- response_body -hello world ---- no_error_log -[error] ---- error_log eval -qr/send data to kafka: \{.*"body":"hello world\\n"/ ---- wait: 2 - - - -=== TEST 33: hit route,expr eval fail ---- request -POST /hello?name=zcxv -abcdef ---- response_body -hello world ---- no_error_log eval -qr/send data to kafka: \{.*"body":"hello world\\n"/ ---- wait: 2 diff --git a/t/plugin/kafka-logger2.t b/t/plugin/kafka-logger2.t new file mode 100644 index 000000000000..73ffec5242e2 --- /dev/null +++ b/t/plugin/kafka-logger2.t @@ -0,0 +1,611 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: required_acks, matches none of the enum values +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.kafka-logger") + local ok, err = plugin.check_schema({ + broker_list = { + ["127.0.0.1"] = 3000 + }, + required_acks = 10, + kafka_topic ="test", + key= "key1" + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +property "required_acks" validation failed: matches none of the enum values +done + + + +=== TEST 2: report log to kafka, with required_acks(1, 0, -1) +--- config +location /t { + content_by_lua_block { + local data = { + { + input = { + plugins = { + ["kafka-logger"] = { + broker_list = { + ["127.0.0.1"] = 9092 + }, + kafka_topic = "test2", + producer_type = "sync", + timeout = 1, + batch_max_size = 1, + required_acks = 1, + meta_format = "origin", + } + }, + upstream = { + nodes = { + ["127.0.0.1:1980"] = 1 + }, + type = "roundrobin" + }, + uri = "/hello", + }, + }, + { + input = { + plugins = { + ["kafka-logger"] = { + broker_list = { + ["127.0.0.1"] = 9092 + }, + kafka_topic = "test2", + producer_type = "sync", + timeout = 1, + batch_max_size = 1, + required_acks = -1, + meta_format = "origin", + } + }, + upstream = { + nodes = { + ["127.0.0.1:1980"] = 1 + }, + type = "roundrobin" + }, + uri = "/hello", + }, + }, + { + input = { + plugins = { + ["kafka-logger"] = { + broker_list = { + ["127.0.0.1"] = 9092 + }, + kafka_topic = "test2", + producer_type = "sync", + timeout = 1, + batch_max_size = 1, + required_acks = 0, + meta_format = "origin", + } + }, + upstream = { + nodes = { + ["127.0.0.1:1980"] = 1 + }, + type = "roundrobin" + }, + uri = "/hello", + }, + }, + } + + local t = require("lib.test_admin").test + local err_count = 0 + for i in ipairs(data) do + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, data[i].input) + + if code >= 300 then + err_count = err_count + 1 + end + ngx.print(body) + + t('/hello', ngx.HTTP_GET) + end + + assert(err_count == 0) + } +} +--- error_log +send data to kafka: GET /hello +send data to kafka: GET /hello +send data to kafka: GET /hello + + + +=== TEST 3: update the broker_list and cluster_name, generate different kafka producers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + ngx.sleep(0.5) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + code, body = t('/apisix/admin/global_rules/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "kafka-logger": { + "broker_list" : { + "127.0.0.1": 9092 + }, + "kafka_topic" : "test2", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false, + "cluster_name": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + code, body = t('/apisix/admin/global_rules/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "kafka-logger": { + "broker_list" : { + "127.0.0.1": 19092 + }, + "kafka_topic" : "test4", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false, + "cluster_name": 2 + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + ngx.sleep(2) + ngx.say("passed") + } + } +--- timeout: 10 +--- response +passed +--- wait: 5 +--- error_log +phase_func(): kafka cluster name 1, broker_list[1] port 9092 +phase_func(): kafka cluster name 2, broker_list[1] port 19092 +--- no_error_log eval +qr/not found topic/ + + + +=== TEST 4: use the topic that does not exist on kafka(even if kafka allows auto create topics, first time push messages to kafka would got this error) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/global_rules/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "kafka-logger": { + "broker_list" : { + "127.0.0.1": 9092 + }, + "kafka_topic" : "undefined_topic", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + ngx.sleep(2) + ngx.say("passed") + } + } +--- timeout: 5 +--- response +passed +--- error_log eval +qr/not found topic, retryable: true, topic: undefined_topic, partition_id: -1/ + + + +=== TEST 5: check broker_list via schema +--- config + location /t { + content_by_lua_block { + local data = { + { + input = { + broker_list = {}, + kafka_topic = "test", + key= "key1", + }, + }, + { + input = { + broker_list = { + ["127.0.0.1"] = "9092" + }, + kafka_topic = "test", + key= "key1", + }, + }, + { + input = { + broker_list = { + ["127.0.0.1"] = 0 + }, + kafka_topic = "test", + key= "key1", + }, + }, + { + input = { + broker_list = { + ["127.0.0.1"] = 65536 + }, + kafka_topic = "test", + key= "key1", + }, + }, + } + + local plugin = require("apisix.plugins.kafka-logger") + + local err_count = 0 + for i in ipairs(data) do + local ok, err = plugin.check_schema(data[i].input) + if not ok then + err_count = err_count + 1 + ngx.say(err) + end + end + + assert(err_count == #data) + } + } +--- response_body +property "broker_list" validation failed: expect object to have at least 1 properties +property "broker_list" validation failed: failed to validate 127.0.0.1 (matching ".*"): wrong type: expected integer, got string +property "broker_list" validation failed: failed to validate 127.0.0.1 (matching ".*"): expected 0 to be greater than 1 +property "broker_list" validation failed: failed to validate 127.0.0.1 (matching ".*"): expected 65536 to be smaller than 65535 + + + +=== TEST 6: kafka brokers info in log +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "kafka-logger": { + "broker_list" : + { + "127.0.0.127":9092 + }, + "kafka_topic" : "test2", + "producer_type": "sync", + "key" : "key1", + "batch_max_size": 1, + "cluster_name": 10 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local res, err = httpc:request_uri(uri, {method = "GET"}) + } + } +--- error_log_like eval +qr/create new kafka producer instance, brokers: \[\{"port":9092,"host":"127.0.0.127"}]/ +qr/failed to send data to Kafka topic: .*, brokers: \{"127.0.0.127":9092}/ + + + +=== TEST 7: set route(id: 1,include_req_body = true,include_req_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "kafka-logger": { + "broker_list" : + { + "127.0.0.1":9092 + }, + "kafka_topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_req_body": true, + "include_req_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- response_body +passed + + + +=== TEST 8: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- error_log eval +qr/send data to kafka: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 9: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to kafka: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 10: check log schema(include_req_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.kafka-logger") + local ok, err = plugin.check_schema({ + kafka_topic = "test", + key = "key1", + broker_list = { + ["127.0.0.1"] = 3 + }, + include_req_body = true, + include_req_body_expr = { + {"bar", "<>", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +failed to validate the 'include_req_body_expr' expression: invalid operator '<>' +done + + + +=== TEST 11: check log schema(include_resp_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.kafka-logger") + local ok, err = plugin.check_schema({ + kafka_topic = "test", + key = "key1", + broker_list = { + ["127.0.0.1"] = 3 + }, + include_resp_body = true, + include_resp_body_expr = { + {"bar", "", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +failed to validate the 'include_resp_body_expr' expression: invalid operator '' +done + + + +=== TEST 12: set route(id: 1,include_resp_body = true,include_resp_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "kafka-logger": { + "broker_list" : + { + "127.0.0.1":9092 + }, + "kafka_topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_resp_body": true, + "include_resp_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- response_body +passed + + + +=== TEST 13: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- error_log eval +qr/send data to kafka: \{.*"body":"hello world\\n"/ +--- wait: 2 + + + +=== TEST 14: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to kafka: \{.*"body":"hello world\\n"/ +--- wait: 2 From 139c3972898f1e45c6d5b1f40143c504116b3486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Wed, 8 Dec 2021 09:28:03 +0800 Subject: [PATCH 170/260] fix(log-rotate): after enabling compression collect log exceptions (#5715) --- apisix/plugins/log-rotate.lua | 202 ++++++++++++++++++++++------------ t/plugin/log-rotate.t | 59 ++++++++-- t/plugin/log-rotate2.t | 54 ++++++++- 3 files changed, 233 insertions(+), 82 deletions(-) diff --git a/apisix/plugins/log-rotate.lua b/apisix/plugins/log-rotate.lua index 273dc01d3c5b..5b711c9dac10 100644 --- a/apisix/plugins/log-rotate.lua +++ b/apisix/plugins/log-rotate.lua @@ -22,12 +22,21 @@ local process = require("ngx.process") local signal = require("resty.signal") local shell = require("resty.shell") local ngx = ngx +local ngx_time = ngx.time +local ngx_update_time = ngx.update_time local lfs = require("lfs") -local io = io -local os = os -local table = table -local string = string -local str_find = core.string.find +local type = type +local io_open = io.open +local os_date = os.date +local os_remove = os.remove +local os_rename = os.rename +local str_sub = string.sub +local str_find = string.find +local str_format = string.format +local str_reverse = string.reverse +local tab_insert = table.insert +local tab_sort = table.sort + local local_conf @@ -35,7 +44,11 @@ local plugin_name = "log-rotate" local INTERVAL = 60 * 60 -- rotate interval (unit: second) local MAX_KEPT = 24 * 7 -- max number of log files will be kept local COMPRESSION_FILE_SUFFIX = ".tar.gz" -- compression file suffix +local rotate_time +local default_logs local enable_compression = false +local DEFAULT_ACCESS_LOG_FILENAME = "access.log" +local DEFAULT_ERROR_LOG_FILENAME = "error.log" local schema = { type = "object", @@ -53,7 +66,7 @@ local _M = { local function file_exists(path) - local file = io.open(path, "r") + local file = io_open(path, "r") if file then file:close() end @@ -62,7 +75,7 @@ end local function get_last_index(str, key) - local rev = string.reverse(str) + local rev = str_reverse(str) local _, idx = str_find(rev, key) local n if idx then @@ -88,15 +101,15 @@ local function get_log_path_info(file_type) local prefix = ngx.config.prefix() if conf_path then - local root = string.sub(conf_path, 1, 1) + local root = str_sub(conf_path, 1, 1) -- relative path if root ~= "/" then conf_path = prefix .. conf_path end local n = get_last_index(conf_path, "/") if n ~= nil and n ~= #conf_path then - local dir = string.sub(conf_path, 1, n) - local name = string.sub(conf_path, n + 1) + local dir = str_sub(conf_path, 1, n) + local name = str_sub(conf_path, n + 1) return dir, name end end @@ -105,50 +118,11 @@ local function get_log_path_info(file_type) end -local function rotate_file(date_str, file_type) - local log_dir, filename = get_log_path_info(file_type) - - core.log.info("rotate log_dir:", log_dir) - core.log.info("rotate filename:", filename) - - local new_filename = date_str .. "__" .. filename - local file_path = log_dir .. new_filename - if file_exists(file_path) then - core.log.info("file exist: ", file_path) - return false - end - - local file_path_org = log_dir .. filename - local ok, msg = os.rename(file_path_org, file_path) - core.log.info("move file from ", file_path_org, " to ", file_path, - " res:", ok, " msg:", msg) - - if ok and enable_compression then - local compression_filename = new_filename .. COMPRESSION_FILE_SUFFIX - local cmd = string.format("cd %s && tar -zcf %s %s", - log_dir, compression_filename, new_filename) - core.log.info("log file compress command: " .. cmd) - local ok, stdout, stderr, reason, status = shell.run(cmd) - core.log.info("compress log file from ", new_filename, " to ", compression_filename, - " res:", ok) - - if ok then - ok = os.remove(file_path) - core.log.warn("remove uncompressed log file: ", file_path, " ret: ", ok) - else - core.log.error("failed to compress log file: ", new_filename, " ret: ", ok, - " stdout: ", stdout, " stderr: ", stderr, " reason: ", reason, " status: ", status) - end - end - - return true -end - - -local function tab_sort(a, b) +local function tab_sort_comp(a, b) return a > b end + local function scan_log_folder() local t = { access = {}, @@ -168,19 +142,83 @@ local function scan_log_folder() if n ~= nil then local log_type = file:sub(n + 2) if log_type == access_name then - table.insert(t.access, file) + tab_insert(t.access, file) elseif log_type == error_name then - table.insert(t.error, file) + tab_insert(t.error, file) end end end - table.sort(t.access, tab_sort) - table.sort(t.error, tab_sort) + tab_sort(t.access, tab_sort_comp) + tab_sort(t.error, tab_sort_comp) return t, log_dir end +local function rename_file(log, date_str) + local new_file + if not log.new_file then + core.log.warn(log.type, " is off") + return + end + + new_file = str_format(log.new_file, date_str) + if file_exists(new_file) then + core.log.info("file exist: ", new_file) + return new_file + end + + local ok, err = os_rename(log.file, new_file) + if not ok then + core.log.error("move file from ", log.file, " to ", new_file, + " res:", ok, " msg:", err) + return + end + + return new_file +end + + +local function compression_file(new_file) + if not new_file or type(new_file) ~= "string" then + core.log.info("compression file: ", new_file, " invalid") + return + end + + local n = get_last_index(new_file, "/") + local new_filepath = str_sub(new_file, 1, n) + local new_filename = str_sub(new_file, n + 1) + local com_filename = new_filename .. COMPRESSION_FILE_SUFFIX + local cmd = str_format("cd %s && tar -zcf %s %s", new_filepath, + com_filename, new_filename) + core.log.info("log file compress command: " .. cmd) + + local ok, stdout, stderr, reason, status = shell.run(cmd) + if not ok then + core.log.error("compress log file from ", new_filename, " to ", com_filename, + " fail, stdout: ", stdout, " stderr: ", stderr, " reason: ", reason, + " status: ", status) + return + end + + ok, stderr = os_remove(new_file) + if stderr then + core.log.error("remove uncompressed log file: ", new_file, + " fail, err: ", stderr, " res:", ok) + end +end + + +local function init_default_logs(logs_info, log_type) + local filepath, filename = get_log_path_info(log_type) + logs_info[log_type] = { type = log_type } + if filename ~= "off" then + logs_info[log_type].file = filepath .. filename + logs_info[log_type].new_file = filepath .. "/%s__" .. filename + end +end + + local function rotate() local interval = INTERVAL local max_kept = MAX_KEPT @@ -194,18 +232,34 @@ local function rotate() core.log.info("rotate interval:", interval) core.log.info("rotate max keep:", max_kept) - local time = ngx.time() - if time % interval == 0 then - time = time - interval - else - time = time - time % interval + if not default_logs then + -- first init default log filepath and filename + default_logs = {} + init_default_logs(default_logs, DEFAULT_ACCESS_LOG_FILENAME) + init_default_logs(default_logs, DEFAULT_ERROR_LOG_FILENAME) end - local date_str = os.date("%Y-%m-%d_%H-%M-%S", time) + ngx_update_time() + local now_time = ngx_time() + if not rotate_time then + -- first init rotate time + rotate_time = now_time + interval + core.log.info("first init rotate time is: ", rotate_time) + return + end + + if now_time < rotate_time then + -- did not reach the rotate time + core.log.info("rotate time: ", rotate_time, " now time: ", now_time) + return + end - local ok1 = rotate_file(date_str, "access.log") - local ok2 = rotate_file(date_str, "error.log") - if not ok1 and not ok2 then + local now_date = os_date("%Y-%m-%d_%H-%M-%S", now_time) + local access_new_file = rename_file(default_logs[DEFAULT_ACCESS_LOG_FILENAME], now_date) + local error_new_file = rename_file(default_logs[DEFAULT_ERROR_LOG_FILENAME], now_date) + if not access_new_file and not error_new_file then + -- reset rotate time + rotate_time = rotate_time + interval return end @@ -216,19 +270,31 @@ local function rotate() core.log.error("failed to send USR1 signal for reopening log file: ", err) end + if enable_compression then + compression_file(access_new_file) + compression_file(error_new_file) + end + -- clean the oldest file local log_list, log_dir = scan_log_folder() for i = max_kept + 1, #log_list.error do local path = log_dir .. log_list.error[i] - local ok = os.remove(path) - core.log.warn("remove old error file: ", path, " ret: ", ok) + ok, err = os_remove(path) + if err then + core.log.error("remove old error file: ", path, " err: ", err, " res:", ok) + end end for i = max_kept + 1, #log_list.access do local path = log_dir .. log_list.access[i] - local ok = os.remove(path) - core.log.warn("remove old access file: ", path, " ret: ", ok) + ok, err = os_remove(path) + if err then + core.log.error("remove old error file: ", path, " err: ", err, " res:", ok) + end end + + -- reset rotate time + rotate_time = rotate_time + interval end diff --git a/t/plugin/log-rotate.t b/t/plugin/log-rotate.t index 94f5ac9e4eae..5e04be131d2d 100644 --- a/t/plugin/log-rotate.t +++ b/t/plugin/log-rotate.t @@ -40,7 +40,16 @@ plugin_attr: _EOC_ $block->set_value("yaml_config", $user_yaml_config); - $block->set_value("request", "GET /t"); + + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + }); run_tests; @@ -81,8 +90,6 @@ __DATA__ } --- error_code eval [200] ---- no_error_log -[error] @@ -97,8 +104,6 @@ __DATA__ } --- response_body done ---- no_error_log -[error] --- error_log start xxxxxx @@ -139,8 +144,6 @@ start xxxxxx } --- response_body done ---- no_error_log -[error] @@ -181,5 +184,43 @@ plugins: --- response_body done true ---- no_error_log -[error] + + + +=== TEST 5: check file changes (disable compression) +--- config + location /t { + content_by_lua_block { + ngx.sleep(2) + + local default_logs = {} + for file_name in lfs.dir(ngx.config.prefix() .. "/logs/") do + if string.match(file_name, "__error.log$") or string.match(file_name, "__access.log$") then + local filepath = ngx.config.prefix() .. "/logs/" .. file_name + local attr = lfs.attributes(filepath) + if attr then + default_logs[filepath] = { change = attr.change, size = attr.size } + end + end + end + + ngx.sleep(1) + + local passed = false + for filepath, origin_attr in pairs(default_logs) do + local check_attr = lfs.attributes(filepath) + if check_attr.change == origin_attr.change and check_attr.size == origin_attr.size then + passed = true + else + passed = false + break + end + end + + if passed then + ngx.say("passed") + end + } + } +--- response_body +passed diff --git a/t/plugin/log-rotate2.t b/t/plugin/log-rotate2.t index d611ffc9a087..b04a6f52aae2 100644 --- a/t/plugin/log-rotate2.t +++ b/t/plugin/log-rotate2.t @@ -41,7 +41,15 @@ plugin_attr: _EOC_ $block->set_value("yaml_config", $user_yaml_config); - $block->set_value("request", "GET /t"); + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + }); run_tests; @@ -74,8 +82,6 @@ __DATA__ end } } ---- no_error_log -[error] @@ -90,7 +96,45 @@ __DATA__ } --- response_body done ---- no_error_log -[error] --- error_log start xxxxxx + + + +=== TEST 3: check file changes (enable compression) +--- config + location /t { + content_by_lua_block { + ngx.sleep(2) + + local default_logs = {} + for file_name in lfs.dir(ngx.config.prefix() .. "/logs/") do + if string.match(file_name, "__error.log.tar.gz$") or string.match(file_name, "__access.log.tar.gz$") then + local filepath = ngx.config.prefix() .. "/logs/" .. file_name + local attr = lfs.attributes(filepath) + if attr then + default_logs[filepath] = { change = attr.change, size = attr.size } + end + end + end + + ngx.sleep(1) + + local passed = false + for filepath, origin_attr in pairs(default_logs) do + local check_attr = lfs.attributes(filepath) + if check_attr.change == origin_attr.change and check_attr.size == origin_attr.size then + passed = true + else + passed = false + break + end + end + + if passed then + ngx.say("passed") + end + } + } +--- response_body +passed From b612f4ec1e102a77e43afb001969850f315ff800 Mon Sep 17 00:00:00 2001 From: "Yu.Bozhong" Date: Wed, 8 Dec 2021 19:14:54 +0800 Subject: [PATCH 171/260] docs(global_rule): update global rule description (#5739) --- docs/en/latest/architecture-design/global-rule.md | 2 +- docs/zh/latest/architecture-design/global-rule.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/architecture-design/global-rule.md b/docs/en/latest/architecture-design/global-rule.md index e27b629cf339..cb9615ea5436 100644 --- a/docs/en/latest/architecture-design/global-rule.md +++ b/docs/en/latest/architecture-design/global-rule.md @@ -21,7 +21,7 @@ title: Global rule # --> -[Plugin](plugin.md) just can be bound to [Service](service.md) or [Route](route.md), if we want a [Plugin](plugin.md) work on all requests, how to do it? +The [Plugin](plugin.md) configuration can be bound directly to a [Route](route.md), or to a [Service](service.md) or [Consumer](consumer.md). What if we want a [Plugin](plugin.md) to work on all requests, how to do it? We can register a global [Plugin](plugin.md) with `GlobalRule`: ```shell diff --git a/docs/zh/latest/architecture-design/global-rule.md b/docs/zh/latest/architecture-design/global-rule.md index de49a2933b2f..63183462b7eb 100644 --- a/docs/zh/latest/architecture-design/global-rule.md +++ b/docs/zh/latest/architecture-design/global-rule.md @@ -21,7 +21,7 @@ title: Global rule # --> -[Plugin](plugin.md) 只能绑定在 [Service](service.md) 或者 [Route](route.md) 上,如果我们需要一个能作用于所有请求的 [Plugin](plugin.md) 该怎么办呢? +[Plugin](plugin.md) 配置可直接绑定在 [Route](route.md) 上,也可以被绑定在 [Service](service.md) 或 [Consumer](consumer.md) 上,如果我们需要一个能作用于所有请求的 [Plugin](plugin.md) 该怎么办呢? 这时候我们可以使用 `GlobalRule` 来注册一个全局的 [Plugin](plugin.md): ```shell From 7b9ffff2ae2854e8e189cbe3e936eb27076c06f4 Mon Sep 17 00:00:00 2001 From: "Yu.Bozhong" Date: Thu, 9 Dec 2021 09:40:23 +0800 Subject: [PATCH 172/260] docs(ldap-auth): add consumer parameters for ldap (#5736) --- docs/en/latest/plugins/ldap-auth.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/en/latest/plugins/ldap-auth.md b/docs/en/latest/plugins/ldap-auth.md index 50d98f5ab69c..39ac8ee8da17 100644 --- a/docs/en/latest/plugins/ldap-auth.md +++ b/docs/en/latest/plugins/ldap-auth.md @@ -41,12 +41,20 @@ This authentication plugin use [lualdap](https://lualdap.github.io/lualdap/) plu ## Attributes -| Name | Type | Requirement | Default | Valid | Description | -| -------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| base_dn | string | required | | | the base dn of the `ldap` server (example : `ou=users,dc=example,dc=org`) | -| ldap_uri | string | required | | | the uri of the ldap server | -| use_tls | boolean | optional | `true` | | Boolean flag indicating if Transport Layer Security (TLS) should be used. | -| uid | string | optional | `cn` | | the `uid` attribute | +For consumer side: + +| Name | Type | Requirement | Default | Valid | Description | +| -------- | ------- | ----------- | ------- | ----- | ----------- | +| user_dn | string | required | | | the user dn of the `ladp` client (example: `cn=user01,ou=users,dc=example,dc=org`) | + +For route side: + +| Name | Type | Requirement | Default | Valid | Description | +| -------- | ------- | ----------- | ------- | ----- | ----------- | +| base_dn | string | required | | | the base dn of the `ldap` server (example : `ou=users,dc=example,dc=org`) | +| ldap_uri | string | required | | | the uri of the ldap server | +| use_tls | boolean | optional | `true` | | Boolean flag indicating if Transport Layer Security (TLS) should be used. | +| uid | string | optional | `cn` | | the `uid` attribute | ## How To Enable From 6d3b06b143ac314549edecda2d341eddaa2f88b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 9 Dec 2021 10:32:13 +0800 Subject: [PATCH 173/260] ci: remove linux_apisix_master_luarocks from the CI on release branch (#5741) --- .github/workflows/cli-master.yml | 58 ++++++++++++++++++++++++++++++++ .github/workflows/cli.yml | 1 - 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cli-master.yml diff --git a/.github/workflows/cli-master.yml b/.github/workflows/cli-master.yml new file mode 100644 index 000000000000..8254ee55ef15 --- /dev/null +++ b/.github/workflows/cli-master.yml @@ -0,0 +1,58 @@ +name: CLI Test (master) + +on: + push: + branches: [master] + paths-ignore: + - 'docs/**' + - '**/*.md' + pull_request: + branches: [master] + paths-ignore: + - 'docs/**' + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_number || github.ref }} + cancel-in-progress: true + +jobs: + build: + strategy: + fail-fast: false + matrix: + job_name: + - linux_apisix_master_luarocks + runs-on: ubuntu-18.04 + timeout-minutes: 15 + env: + OPENRESTY_VERSION: default + + steps: + - name: Check out code + uses: actions/checkout@v2.4.0 + with: + submodules: recursive + + - name: Cache deps + uses: actions/cache@v2.1.7 + env: + cache-name: cache-deps + with: + path: deps + key: ${{ runner.os }}-${{ env.cache-name }}-${{ matrix.job_name }}-${{ hashFiles('rockspec/apisix-master-0.rockspec') }} + + - name: Linux launch common services + run: | + project_compose_ci=ci/pod/docker-compose.common.yml make ci-env-up + + - name: Linux Get dependencies + run: sudo apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl libpcre3 libpcre3-dev libldap2-dev + + - name: Linux Install + run: | + sudo --preserve-env=OPENRESTY_VERSION \ + ./ci/${{ matrix.job_name }}_runner.sh do_install + + - name: Linux Script + run: sudo ./ci/${{ matrix.job_name }}_runner.sh script diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index bb9832bfb8ae..32f3feed5306 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -24,7 +24,6 @@ jobs: platform: - ubuntu-18.04 job_name: - - linux_apisix_master_luarocks - linux_apisix_current_luarocks - linux_apisix_current_luarocks_in_customed_nginx From 87f662603d55eb1887f24fe435e60c58b99945ea Mon Sep 17 00:00:00 2001 From: zhang lun hai Date: Thu, 9 Dec 2021 10:58:56 +0800 Subject: [PATCH 174/260] feat: add http_server_location_configuration_snippet configuration (#5740) Co-authored-by: lunhaiz --- apisix/cli/ngx_tpl.lua | 6 ++++++ conf/config-default.yaml | 3 +++ t/cli/test_snippet.sh | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index f5fa5d6ee06d..523ae4d7ef76 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -573,6 +573,12 @@ http { set $ctx_ref ''; set $from_error_page ''; + # http server location configuration snippet starts + {% if http_server_location_configuration_snippet then %} + {* http_server_location_configuration_snippet *} + {% end %} + # http server location configuration snippet ends + {% if enabled_plugins["dubbo-proxy"] then %} set $dubbo_service_name ''; set $dubbo_service_version ''; diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 7be15f9267b0..3d089bdbf052 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -191,6 +191,9 @@ nginx_config: # config for render the template to generate n http_server_configuration_snippet: | # Add custom Nginx http server configuration to nginx.conf. # The configuration should be well indented! + http_server_location_configuration_snippet: | + # Add custom Nginx http server location configuration to nginx.conf. + # The configuration should be well indented! http_admin_configuration_snippet: | # Add custom Nginx admin server configuration to nginx.conf. # The configuration should be well indented! diff --git a/t/cli/test_snippet.sh b/t/cli/test_snippet.sh index e103224236f5..0684d6c1f659 100755 --- a/t/cli/test_snippet.sh +++ b/t/cli/test_snippet.sh @@ -37,6 +37,8 @@ nginx_config: chunked_transfer_encoding on; http_server_configuration_snippet: | set $my "var"; + http_server_location_configuration_snippet: | + set $upstream_name -; http_admin_configuration_snippet: | log_format admin "$request_time $pipe"; http_end_configuration_snippet: | @@ -65,6 +67,12 @@ if [ ! $? -eq 0 ]; then exit 1 fi +grep 'set $upstream_name -;' -A 2 conf/nginx.conf | grep "configuration snippet ends" > /dev/null +if [ ! $? -eq 0 ]; then + echo "failed: can't inject http server location configuration" + exit 1 +fi + grep 'log_format admin "$request_time $pipe";' -A 2 conf/nginx.conf | grep "configuration snippet ends" > /dev/null if [ ! $? -eq 0 ]; then echo "failed: can't inject admin server configuration" From 5ae38f81f25fc58a040e30d12ba8fef33bb56f60 Mon Sep 17 00:00:00 2001 From: yuz10 <845238369@qq.com> Date: Thu, 9 Dec 2021 14:34:06 +0800 Subject: [PATCH 175/260] fix: bump lua-resty-rocketmq to 0.2.1-3 (#5744) --- rockspec/apisix-master-0.rockspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index 444471be4919..2c4ddf5d6fa8 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -72,7 +72,7 @@ dependencies = { "api7-snowflake = 2.0-1", "inspect == 3.1.1", "lualdap = 1.2.6-1", - "lua-resty-rocketmq = 0.2.1-1", + "lua-resty-rocketmq = 0.2.1-3", } build = { From c178435d7ada4eeb713d9a1688fb5f54f971abdf Mon Sep 17 00:00:00 2001 From: Gaoll Date: Thu, 9 Dec 2021 21:58:50 +0800 Subject: [PATCH 176/260] feat(consumer-restriction): customize rejected_msg (#5732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: gaoliangliang Co-authored-by: 高亮亮 --- apisix/plugins/consumer-restriction.lua | 8 ++++++-- docs/en/latest/plugins/consumer-restriction.md | 1 + docs/zh/latest/plugins/consumer-restriction.md | 1 + t/plugin/consumer-restriction.t | 5 +++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/consumer-restriction.lua b/apisix/plugins/consumer-restriction.lua index 8e540ae697da..a9f7363a5176 100644 --- a/apisix/plugins/consumer-restriction.lua +++ b/apisix/plugins/consumer-restriction.lua @@ -51,7 +51,8 @@ local schema = { } } }, - rejected_code = {type = "integer", minimum = 200, default = 403} + rejected_code = {type = "integer", minimum = 200, default = 403}, + rejected_msg = {type = "string"} }, anyOf = { {required = {"blacklist"}}, @@ -105,7 +106,10 @@ local function is_method_allowed(allowed_methods, method, user) end local function reject(conf) - return conf.rejected_code, { message = "The " .. conf.type .. " is forbidden." } + if conf.rejected_msg then + return conf.rejected_code , { message = conf.rejected_msg } + end + return conf.rejected_code , { message = "The " .. conf.type .. " is forbidden."} end function _M.check_schema(conf) diff --git a/docs/en/latest/plugins/consumer-restriction.md b/docs/en/latest/plugins/consumer-restriction.md index bd36daaf890b..4e6cd1a638a5 100644 --- a/docs/en/latest/plugins/consumer-restriction.md +++ b/docs/en/latest/plugins/consumer-restriction.md @@ -42,6 +42,7 @@ The `consumer-restriction` makes corresponding access restrictions based on diff | whitelist | array[string] | required | | | Grant full access to all users specified in the provided list , **has the priority over `allowed_by_methods`** | | blacklist | array[string] | required | | | Reject connection to all users specified in the provided list , **has the priority over `whitelist`** | | rejected_code | integer | optional | 403 | [200,...] | The HTTP status code returned when the request is rejected. | +| rejected_msg | string | optional | | | The message returned when the request is rejected. | | allowed_by_methods | array[object] | optional | | | Set a list of allowed HTTP methods for the selected user , HTTP methods can be `["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"]` | For the `type` field is an enumerated type, it can be `consumer_name` or `service_id`. They stand for the following meanings: diff --git a/docs/zh/latest/plugins/consumer-restriction.md b/docs/zh/latest/plugins/consumer-restriction.md index 64d43d2acfd5..92a2627bf540 100644 --- a/docs/zh/latest/plugins/consumer-restriction.md +++ b/docs/zh/latest/plugins/consumer-restriction.md @@ -42,6 +42,7 @@ title: consumer-restriction | whitelist | array[string] | 必选 | | | 与`blacklist`二选一,只能单独启用白名单或黑名单,两个不能一起使用。 | | blacklist | array[string] | 必选 | | | 与`whitelist`二选一,只能单独启用白名单或黑名单,两个不能一起使用。 | | rejected_code | integer | 可选 | 403 | [200,...] | 当请求被拒绝时,返回的 HTTP 状态码。| +| rejected_msg | String | 可选 | | | 当请求被拒绝时,返回的消息内容。| 对于 `type` 字段是个枚举类型,它可以是 `consumer_name` 或 `service_id` 。分别代表以下含义: diff --git a/t/plugin/consumer-restriction.t b/t/plugin/consumer-restriction.t index 21c26ccdbbcd..5fc39e045b83 100644 --- a/t/plugin/consumer-restriction.t +++ b/t/plugin/consumer-restriction.t @@ -263,7 +263,8 @@ Authorization: Basic amFjazIwMjA6MTIzNDU2 "consumer-restriction": { "blacklist": [ "jack1" - ] + ], + "rejected_msg": "request is forbidden" } } }]] @@ -302,7 +303,7 @@ GET /hello Authorization: Basic amFjazIwMTk6MTIzNDU2 --- error_code: 403 --- response_body -{"message":"The consumer_name is forbidden."} +{"message":"request is forbidden"} --- no_error_log [error] From 08a363d2c3a0b4d34d144bf205e8f5dc5cc63d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 10 Dec 2021 11:06:37 +0800 Subject: [PATCH 177/260] chore: bump apisix-build-tools to v2.6.0 (#5748) --- .github/workflows/centos7-ci.yml | 2 +- utils/linux-install-openresty.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index db5ad0b1e041..c1362023a42e 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -44,7 +44,7 @@ jobs: run: | export VERSION=${{ steps.branch_env.outputs.version }} sudo gem install --no-document fpm - git clone -b v2.5.0 https://github.com/api7/apisix-build-tools.git + git clone -b v2.6.0 https://github.com/api7/apisix-build-tools.git # move codes under build tool mkdir ./apisix-build-tools/apisix diff --git a/utils/linux-install-openresty.sh b/utils/linux-install-openresty.sh index ce0d8a48bcb8..017ebf52d661 100755 --- a/utils/linux-install-openresty.sh +++ b/utils/linux-install-openresty.sh @@ -26,7 +26,7 @@ sudo apt-get update if [ "$OPENRESTY_VERSION" == "source" ]; then cd .. - wget https://raw.githubusercontent.com/api7/apisix-build-tools/v2.5.0/build-apisix-base.sh + wget https://raw.githubusercontent.com/api7/apisix-build-tools/v2.6.0/build-apisix-base.sh chmod +x build-apisix-base.sh ./build-apisix-base.sh latest From 8c6b782d061e377200573c0fc731d0e33d491754 Mon Sep 17 00:00:00 2001 From: yuz10 <845238369@qq.com> Date: Fri, 10 Dec 2021 15:50:52 +0800 Subject: [PATCH 178/260] test(rocketmq-logger): reduce duplicate sections (#5743) --- t/plugin/rocketmq-logger.t | 465 ++---------------------------------- t/plugin/rocketmq-logger2.t | 414 ++++++++++++++++++++++++++++++++ 2 files changed, 435 insertions(+), 444 deletions(-) create mode 100644 t/plugin/rocketmq-logger2.t diff --git a/t/plugin/rocketmq-logger.t b/t/plugin/rocketmq-logger.t index 0ac81229a0cb..79b90136b438 100644 --- a/t/plugin/rocketmq-logger.t +++ b/t/plugin/rocketmq-logger.t @@ -19,6 +19,19 @@ use t::APISIX 'no_plan'; repeat_each(1); no_long_string(); no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + run_tests; __DATA__ @@ -45,8 +58,6 @@ __DATA__ GET /t --- response_body done ---- no_error_log -[error] @@ -67,8 +78,6 @@ GET /t --- response_body property "nameserver_list" is required done ---- no_error_log -[error] @@ -96,8 +105,6 @@ GET /t --- response_body property "timeout" validation failed: wrong type: expected integer, got string done ---- no_error_log -[error] @@ -161,8 +168,6 @@ done GET /t --- response_body passed ---- no_error_log -[error] @@ -171,8 +176,7 @@ passed GET /hello --- response_body hello world ---- no_error_log -[error] + --- wait: 2 @@ -284,8 +288,6 @@ failed to send data to rocketmq topic GET /t --- response_body passed ---- no_error_log -[error] @@ -295,8 +297,7 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] + --- error_log send data to rocketmq: GET /hello?ab=cd HTTP/1.1 host: localhost @@ -346,8 +347,6 @@ abcdef GET /t --- response_body passed ---- no_error_log -[error] @@ -357,8 +356,7 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] + --- error_log send data to rocketmq: GET /hello?ab=cd HTTP/1.1 host: localhost @@ -405,8 +403,6 @@ connection: close GET /t --- response_body passed ---- no_error_log -[error] @@ -416,8 +412,7 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] + --- error_log_like eval qr/send data to rocketmq: \{.*"upstream":"127.0.0.1:1980"/ --- wait: 2 @@ -482,8 +477,6 @@ qr/send data to rocketmq: \{.*"upstream":"127.0.0.1:1980"/ GET /t --- response_body passed ---- no_error_log -[error] @@ -492,8 +485,7 @@ passed GET /hello --- response_body hello world ---- no_error_log -[error] + --- wait: 2 @@ -534,8 +526,6 @@ hello world GET /t --- response_body passed ---- no_error_log -[error] @@ -545,8 +535,7 @@ GET /hello?ab=cd abcdef --- response_body hello world ---- no_error_log -[error] + --- error_log_like eval qr/send data to rocketmq: \{.*"upstream":"127.0.0.1:1980"/ --- wait: 2 @@ -589,8 +578,6 @@ qr/send data to rocketmq: \{.*"upstream":"127.0.0.1:1980"/ GET /t --- response_body passed ---- no_error_log -[error] @@ -634,8 +621,7 @@ passed GET /t --- timeout: 5s --- ignore_response ---- no_error_log -[error] + --- error_log eval [qr/queue: 1/, qr/queue: 0/, @@ -682,417 +668,8 @@ qr/queue: 2/] GET /t --- timeout: 5s --- ignore_response ---- no_error_log -[error] + --- error_log eval [qr/queue: 1/, qr/queue: 0/, qr/queue: 2/] - - - -=== TEST 20: update the nameserver_list, generate different rocketmq producers ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]] - ) - ngx.sleep(0.5) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - code, body = t('/apisix/admin/routes/1/plugins', - ngx.HTTP_PATCH, - [[{ - "rocketmq-logger": { - "nameserver_list" : [ "127.0.0.1:9876" ], - "topic" : "test2", - "timeout" : 1, - "batch_max_size": 1, - "include_req_body": false - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - t('/hello',ngx.HTTP_GET) - ngx.sleep(0.5) - - code, body = t('/apisix/admin/routes/1/plugins', - ngx.HTTP_PATCH, - [[{ - "rocketmq-logger": { - "nameserver_list" : [ "127.0.0.1:19876" ], - "topic" : "test4", - "timeout" : 1, - "batch_max_size": 1, - "include_req_body": false - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - t('/hello',ngx.HTTP_GET) - ngx.sleep(0.5) - - ngx.sleep(2) - ngx.say("passed") - } - } ---- request -GET /t ---- timeout: 10 ---- response -passed ---- wait: 5 ---- error_log -phase_func(): rocketmq nameserver_list[1] port 9876 -phase_func(): rocketmq nameserver_list[1] port 19876 ---- no_error_log eval -qr/not found topic/ - - - -=== TEST 21: use the topic that does not exist on rocketmq(even if rocketmq allows auto create topics, first time push messages to rocketmq would got this error) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1/plugins', - ngx.HTTP_PATCH, - [[{ - "rocketmq-logger": { - "nameserver_list" : [ "127.0.0.1:9876" ], - "topic" : "undefined_topic", - "timeout" : 1, - "batch_max_size": 1, - "include_req_body": false - } - }]] - ) - - if code >= 300 then - ngx.status = code - ngx.say("fail") - return - end - - t('/hello',ngx.HTTP_GET) - ngx.sleep(0.5) - - ngx.sleep(2) - ngx.say("passed") - } - } ---- request -GET /t ---- timeout: 5 ---- response -passed ---- error_log eval -qr/getTopicRouteInfoFromNameserver return TOPIC_NOT_EXIST, No topic route info in name server for the topic: undefined_topic/ - - - -=== TEST 22: rocketmq nameserver list info in log ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "rocketmq-logger": { - "nameserver_list" : [ "127.0.0.1:9876" ], - "topic" : "test2", - "producer_type": "sync", - "key" : "key1", - "batch_max_size": 1 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - local http = require "resty.http" - local httpc = http.new() - local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" - local res, err = httpc:request_uri(uri, {method = "GET"}) - } - } ---- request -GET /t ---- error_log_like eval -qr/create new rocketmq producer instance, nameserver_list: \[\{"port":9876,"host":"127.0.0.127"}]/ -qr/failed to send data to rocketmq topic: .*, nameserver_list: \{"127.0.0.127":9876}/ - - - -=== TEST 23: delete plugin metadata, tests would fail if run rocketmq-logger-log-format.t and plugin metadata is added ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/plugin_metadata/rocketmq-logger', - ngx.HTTP_DELETE, - nil, - [[{"action": "delete"}]]) - } - } ---- request -GET /t ---- response_body - ---- no_error_log -[error] - - - -=== TEST 24: set route(id: 1,include_req_body = true,include_req_body_expr = array) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [=[{ - "plugins": { - "rocketmq-logger": { - "nameserver_list" : [ "127.0.0.1:9876" ], - "topic" : "test2", - "key" : "key1", - "timeout" : 1, - "include_req_body": true, - "include_req_body_expr": [ - [ - "arg_name", - "==", - "qwerty" - ] - ], - "batch_max_size": 1 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]=] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } - ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 25: hit route, expr eval success ---- request -POST /hello?name=qwerty -abcdef ---- response_body -hello world ---- no_error_log -[error] ---- error_log eval -qr/send data to rocketmq: \{.*"body":"abcdef"/ ---- wait: 2 - - - -=== TEST 26: hit route,expr eval fail ---- request -POST /hello?name=zcxv -abcdef ---- response_body -hello world ---- no_error_log eval -qr/send data to rocketmq: \{.*"body":"abcdef"/ ---- wait: 2 - - - -=== TEST 27: check log schema(include_req_body) ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.rocketmq-logger") - local ok, err = plugin.check_schema({ - topic = "test", - key = "key1", - nameserver_list = { - "127.0.0.1:3" - }, - include_req_body = true, - include_req_body_expr = { - {"bar", "<>", "foo"} - } - }) - if not ok then - ngx.say(err) - end - ngx.say("done") - } - } ---- request -GET /t ---- response_body -failed to validate the 'include_req_body_expr' expression: invalid operator '<>' -done ---- no_error_log -[error] - - - -=== TEST 28: check log schema(include_resp_body) ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.rocketmq-logger") - local ok, err = plugin.check_schema({ - topic = "test", - key = "key1", - nameserver_list = { - "127.0.0.1:3" - }, - include_resp_body = true, - include_resp_body_expr = { - {"bar", "", "foo"} - } - }) - if not ok then - ngx.say(err) - end - ngx.say("done") - } - } ---- request -GET /t ---- response_body -failed to validate the 'include_resp_body_expr' expression: invalid operator '' -done ---- no_error_log -[error] - - - -=== TEST 29: set route(id: 1,include_resp_body = true,include_resp_body_expr = array) ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [=[{ - "plugins": { - "rocketmq-logger": { - "nameserver_list" : [ "127.0.0.1:9876" ], - "topic" : "test2", - "key" : "key1", - "timeout" : 1, - "include_resp_body": true, - "include_resp_body_expr": [ - [ - "arg_name", - "==", - "qwerty" - ] - ], - "batch_max_size": 1 - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]=] - ) - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } - ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 30: hit route, expr eval success ---- request -POST /hello?name=qwerty -abcdef ---- response_body -hello world ---- no_error_log -[error] ---- error_log eval -qr/send data to rocketmq: \{.*"body":"hello world\\n"/ ---- wait: 2 - - - -=== TEST 31: hit route,expr eval fail ---- request -POST /hello?name=zcxv -abcdef ---- response_body -hello world ---- no_error_log eval -qr/send data to rocketmq: \{.*"body":"hello world\\n"/ ---- wait: 2 diff --git a/t/plugin/rocketmq-logger2.t b/t/plugin/rocketmq-logger2.t new file mode 100644 index 000000000000..fcc378b70269 --- /dev/null +++ b/t/plugin/rocketmq-logger2.t @@ -0,0 +1,414 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: update the nameserver_list, generate different rocketmq producers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + ngx.sleep(0.5) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + code, body = t('/apisix/admin/routes/1/plugins', + ngx.HTTP_PATCH, + [[{ + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + code, body = t('/apisix/admin/routes/1/plugins', + ngx.HTTP_PATCH, + [[{ + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:19876" ], + "topic" : "test4", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + ngx.sleep(2) + ngx.say("passed") + } + } +--- timeout: 10 +--- response +passed +--- wait: 5 +--- error_log +phase_func(): rocketmq nameserver_list[1] port 9876 +phase_func(): rocketmq nameserver_list[1] port 19876 +--- no_error_log eval +qr/not found topic/ + + + +=== TEST 2: use the topic that does not exist on rocketmq(even if rocketmq allows auto create topics, first time push messages to rocketmq would got this error) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1/plugins', + ngx.HTTP_PATCH, + [[{ + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "undefined_topic", + "timeout" : 1, + "batch_max_size": 1, + "include_req_body": false + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say("fail") + return + end + + t('/hello',ngx.HTTP_GET) + ngx.sleep(0.5) + + ngx.sleep(2) + ngx.say("passed") + } + } +--- timeout: 5 +--- response +passed +--- error_log eval +qr/getTopicRouteInfoFromNameserver return TOPIC_NOT_EXIST, No topic route info in name server for the topic: undefined_topic/ + + + +=== TEST 3: rocketmq nameserver list info in log +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "producer_type": "sync", + "key" : "key1", + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + local http = require "resty.http" + local httpc = http.new() + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local res, err = httpc:request_uri(uri, {method = "GET"}) + } + } +--- error_log_like eval +qr/create new rocketmq producer instance, nameserver_list: \[\{"port":9876,"host":"127.0.0.127"}]/ +qr/failed to send data to rocketmq topic: .*, nameserver_list: \{"127.0.0.127":9876}/ + + + +=== TEST 4: delete plugin metadata, tests would fail if run rocketmq-logger-log-format.t and plugin metadata is added +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/rocketmq-logger', + ngx.HTTP_DELETE, + nil, + [[{"action": "delete"}]]) + } + } +--- response_body + + + +=== TEST 5: set route(id: 1,include_req_body = true,include_req_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_req_body": true, + "include_req_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- response_body +passed + + + +=== TEST 6: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world + +--- error_log eval +qr/send data to rocketmq: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 7: hit route,expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to rocketmq: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 8: check log schema(include_req_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + topic = "test", + key = "key1", + nameserver_list = { + "127.0.0.1:3" + }, + include_req_body = true, + include_req_body_expr = { + {"bar", "<>", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +failed to validate the 'include_req_body_expr' expression: invalid operator '<>' +done + + + +=== TEST 9: check log schema(include_resp_body) +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + topic = "test", + key = "key1", + nameserver_list = { + "127.0.0.1:3" + }, + include_resp_body = true, + include_resp_body_expr = { + {"bar", "", "foo"} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +failed to validate the 'include_resp_body_expr' expression: invalid operator '' +done + + + +=== TEST 10: set route(id: 1,include_resp_body = true,include_resp_body_expr = array) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "plugins": { + "rocketmq-logger": { + "nameserver_list" : [ "127.0.0.1:9876" ], + "topic" : "test2", + "key" : "key1", + "timeout" : 1, + "include_resp_body": true, + "include_resp_body_expr": [ + [ + "arg_name", + "==", + "qwerty" + ] + ], + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]=] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + +--- response_body +passed + + + +=== TEST 11: hit route, expr eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world + +--- error_log eval +qr/send data to rocketmq: \{.*"body":"hello world\\n"/ +--- wait: 2 + + + +=== TEST 12: hit route, expr eval fail +--- request +POST /hello?name=zcxv +abcdef +--- response_body +hello world +--- no_error_log eval +qr/send data to rocketmq: \{.*"body":"hello world\\n"/ +--- wait: 2 From 4525437880f8342560bbfbfe8711c504f7b47584 Mon Sep 17 00:00:00 2001 From: yuz10 <845238369@qq.com> Date: Sun, 12 Dec 2021 17:20:46 +0800 Subject: [PATCH 179/260] chore: bump lua-resty-rocketmq to 0.3.0-0 (#5774) --- rockspec/apisix-master-0.rockspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index 2c4ddf5d6fa8..d2186dfc520b 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -72,7 +72,7 @@ dependencies = { "api7-snowflake = 2.0-1", "inspect == 3.1.1", "lualdap = 1.2.6-1", - "lua-resty-rocketmq = 0.2.1-3", + "lua-resty-rocketmq = 0.3.0-0", } build = { From 02d32c9dbc0813e68f51075b2ff8a067f60fca6a Mon Sep 17 00:00:00 2001 From: kerneltravel Date: Sun, 12 Dec 2021 20:03:43 +0800 Subject: [PATCH 180/260] docs: remove duplicated word (#5773) --- docs/zh/latest/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/latest/FAQ.md b/docs/zh/latest/FAQ.md index 2f245e27e364..816093f3fc62 100644 --- a/docs/zh/latest/FAQ.md +++ b/docs/zh/latest/FAQ.md @@ -566,5 +566,5 @@ apisix: `plugin-metadata` 和 `plugin-configs` 的区别在于: - - 插件实例作用范围:`plugin-metadata` 作用于该插件的所有配置实例。`plugin-configs` 作用于其下配置的的插件配置实例。 + - 插件实例作用范围:`plugin-metadata` 作用于该插件的所有配置实例。`plugin-configs` 作用于其下配置的插件配置实例。 - 绑定主体作用范围:`plugin-metadata` 作用于该插件的所有配置实例绑定的主体。`plugin-configs` 作用于绑定了该 `plugin-configs` 的路由。 From 71c256be81d95d56ea38f9874699801cbbad6a2e Mon Sep 17 00:00:00 2001 From: Bisakh Date: Mon, 13 Dec 2021 06:56:48 +0530 Subject: [PATCH 181/260] feat: enable L4 stream logging (#5768) --- apisix/cli/ngx_tpl.lua | 7 +++++++ conf/config-default.yaml | 5 +++++ t/cli/test_access_log.sh | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 523ae4d7ef76..66c18372bfc8 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -84,6 +84,13 @@ stream { lua_ssl_trusted_certificate {* ssl.ssl_trusted_certificate *}; {% end %} + # for stream logs, off by default + {% if stream.enable_access_log == true then %} + log_format main escape={* stream.access_log_format_escape *} '{* stream.access_log_format *}'; + + access_log {* stream.access_log *} main buffer=16384 flush=3; + {% end %} + # stream configuration snippet starts {% if stream_configuration_snippet then %} {* stream_configuration_snippet *} diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 3d089bdbf052..84fe59b41d14 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -174,6 +174,11 @@ nginx_config: # config for render the template to generate n # - TEST_ENV stream: + enable_access_log: false # enable access log or not, default false + access_log: logs/access_stream.log + access_log_format: "$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time" + # create your custom log format by visiting http://nginx.org/en/docs/varindex.html + access_log_format_escape: default # allows setting json or default characters escaping in variables lua_shared_dict: etcd-cluster-health-check-stream: 10m lrucache-lock-stream: 10m diff --git a/t/cli/test_access_log.sh b/t/cli/test_access_log.sh index 8e79b009f405..1f4cfd544cda 100755 --- a/t/cli/test_access_log.sh +++ b/t/cli/test_access_log.sh @@ -224,3 +224,40 @@ fi make stop echo "passed: should find upstream scheme" + +# check stream logs +echo ' +apisix: + stream_proxy: # UDP proxy + udp: + - "127.0.0.1:9200" + +nginx_config: + stream: + enable_access_log: true + access_log_format: "$remote_addr $protocol test_stream_access_log_format" +' > conf/config.yaml + +make init + +grep "test_stream_access_log_format" conf/nginx.conf > /dev/null +if [ ! $? -eq 0 ]; then + echo "failed: stream access_log_format in nginx.conf doesn't change" + exit 1 +fi +echo "passed: stream access_log_format in nginx.conf is ok" + +# check if logs are being written +make run +sleep 0.1 +# sending single udp packet +echo -n "hello" | nc -4u -w0 localhost 9200 +sleep 4 +tail -n 1 logs/access_stream.log > output.log + +if ! grep '127.0.0.1 UDP test_stream_access_log_format' output.log; then + echo "failed: should have found udp log entry" + cat output.log + exit 1 +fi +echo "passed: logs are being dumped for stream proxy" From 14ad3f331e67c452af30eb0f0ff0837673dfe323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 13 Dec 2021 09:38:54 +0800 Subject: [PATCH 182/260] chore: depend apisix-base in Centos (#5776) --- utils/install-dependencies.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh index 85f9ff75cf7b..6ffaba510ec9 100755 --- a/utils/install-dependencies.sh +++ b/utils/install-dependencies.sh @@ -44,12 +44,24 @@ function install_dependencies_with_aur() { # Install dependencies on centos and fedora function install_dependencies_with_yum() { - # add OpenResty source sudo yum install yum-utils - sudo yum-config-manager --add-repo "https://openresty.org/package/${1}/openresty.repo" - # install OpenResty and some compilation tools - sudo yum install -y openresty curl git gcc openresty-openssl111-devel unzip pcre pcre-devel openldap-devel + local common_dep="curl git gcc openresty-openssl111-devel unzip pcre pcre-devel openldap-devel" + if [ "${1}" == "centos" ]; then + # add APISIX source + sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo + + # install apisix-base and some compilation tools + # shellcheck disable=SC2086 + sudo yum install -y apisix-base $common_dep + else + # add OpenResty source + sudo yum-config-manager --add-repo "https://openresty.org/package/${1}/openresty.repo" + + # install OpenResty and some compilation tools + # shellcheck disable=SC2086 + sudo yum install -y openresty $common_dep + fi } # Install dependencies on ubuntu and debian From 1e53ccb3ae925281bbedcfec85efcacb13454e4e Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Mon, 13 Dec 2021 10:00:40 +0800 Subject: [PATCH 183/260] feat: basic support OPA plugin (#5734) --- Makefile | 3 + apisix/core/request.lua | 17 +++-- apisix/plugins/opa.lua | 103 +++++++++++++++++++++++++++++++ apisix/plugins/opa/helper.lua | 61 ++++++++++++++++++ ci/linux-ci-init-service.sh | 11 ++++ ci/pod/docker-compose.yml | 11 ++++ conf/config-default.yaml | 1 + t/admin/plugins.t | 1 + t/core/request.t | 27 ++++++++ t/plugin/opa.t | 113 ++++++++++++++++++++++++++++++++++ 10 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 apisix/plugins/opa.lua create mode 100644 apisix/plugins/opa/helper.lua create mode 100644 t/plugin/opa.t diff --git a/Makefile b/Makefile index ac08efa00067..adcd64dfed11 100644 --- a/Makefile +++ b/Makefile @@ -318,6 +318,9 @@ install: runtime $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/zipkin $(ENV_INSTALL) apisix/plugins/zipkin/*.lua $(ENV_INST_LUADIR)/apisix/plugins/zipkin/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/plugins/opa + $(ENV_INSTALL) apisix/plugins/opa/*.lua $(ENV_INST_LUADIR)/apisix/plugins/opa/ + $(ENV_INSTALL) -d $(ENV_INST_LUADIR)/apisix/ssl/router $(ENV_INSTALL) apisix/ssl/router/*.lua $(ENV_INST_LUADIR)/apisix/ssl/router/ diff --git a/apisix/core/request.lua b/apisix/core/request.lua index 95d84b95b4f6..69ff2fa30fd0 100644 --- a/apisix/core/request.lua +++ b/apisix/core/request.lua @@ -21,10 +21,10 @@ local io = require("apisix.core.io") local ngx = ngx local get_headers = ngx.req.get_headers local clear_header = ngx.req.clear_header -local tonumber = tonumber -local error = error -local type = type -local str_fmt = string.format +local tonumber = tonumber +local error = error +local type = type +local str_fmt = string.format local str_lower = string.lower local req_read_body = ngx.req.read_body local req_get_body_data = ngx.req.get_body_data @@ -269,6 +269,15 @@ function _M.get_port(ctx) end +function _M.get_path(ctx) + if not ctx then + ctx = ngx.ctx.api_ctx + end + + return ctx.var.uri or '' +end + + function _M.get_http_version() return ngx.req.http_version() end diff --git a/apisix/plugins/opa.lua b/apisix/plugins/opa.lua new file mode 100644 index 000000000000..f33c3d042b2a --- /dev/null +++ b/apisix/plugins/opa.lua @@ -0,0 +1,103 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +local core = require("apisix.core") +local http = require("resty.http") +local helper = require("apisix.plugins.opa.helper") + +local schema = { + type = "object", + properties = { + host = {type = "string"}, + ssl_verify = { + type = "boolean", + default = true, + }, + policy = {type = "string"}, + timeout = { + type = "integer", + minimum = 1, + maximum = 60000, + default = 3000, + description = "timeout in milliseconds", + }, + keepalive = {type = "boolean", default = true}, + keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, + keepalive_pool = {type = "integer", minimum = 1, default = 5} + }, + required = {"host", "policy"} +} + + +local _M = { + version = 0.1, + priority = 2001, + name = "opa", + schema = schema, +} + + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + + +function _M.access(conf, ctx) + local body = helper.build_opa_input(conf, ctx, "http") + local params = { + method = "POST", + body = body, + headers = { + ["Content-Type"] = "application/json", + }, + keepalive = conf.keepalive, + ssl_verify = conf.ssl_verify + } + + if conf.keepalive then + params.keepalive_timeout = conf.keepalive_timeout + params.keepalive_pool = conf.keepalive_pool + end + + local endpoint = conf.host .. "/v1/data/" .. conf.policy + + local httpc = http.new() + httpc:set_timeout(conf.timeout) + + local res, err = httpc:request_uri(endpoint, params) + + -- block by default when decision is unavailable + if not res or err then + core.log.error("failed to process OPA decision, err: ", err) + return 403 + end + + -- parse the results of the decision + local data, err = core.json.decode(res.body) + + if err then + core.log.error("invalid response body: ", res.body, " err: ", err) + return 503 + end + + if not data.result then + return 403 + end +end + + +return _M diff --git a/apisix/plugins/opa/helper.lua b/apisix/plugins/opa/helper.lua new file mode 100644 index 000000000000..2a8cf94316b4 --- /dev/null +++ b/apisix/plugins/opa/helper.lua @@ -0,0 +1,61 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +local core = require("apisix.core") +local ngx_time = ngx.time + +local _M = {} + + +local function build_var(conf, ctx) + return { + server_addr = ctx.var.server_addr, + server_port = ctx.var.server_port, + remote_addr = ctx.var.remote_addr, + remote_port = ctx.var.remote_port, + timestamp = ngx_time(), + } +end + + +local function build_http_request(conf, ctx) + return { + scheme = core.request.get_scheme(ctx), + method = core.request.get_method(ctx), + host = core.request.get_host(ctx), + port = core.request.get_port(ctx), + path = core.request.get_path(ctx), + header = core.request.headers(ctx), + query = core.request.get_uri_args(ctx), + } +end + + +function _M.build_opa_input(conf, ctx, subsystem) + local request = build_http_request(conf, ctx) + + local data = { + type = subsystem, + request = request, + var = build_var(conf, ctx) + } + + return core.json.encode({input = data}) +end + + +return _M diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 0c3ff5d03096..2939e827d2bf 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -31,3 +31,14 @@ docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test2 -c DefaultCluster docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test3 -c DefaultCluster docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test4 -c DefaultCluster + +# prepare OPA env +curl -XPUT 'http://localhost:8181/v1/policies/example' \ +--header 'Content-Type: text/plain' \ +--data-raw 'package example + +default allow = false + +allow { + input.request.header["test-header"] == "only-for-test" +}' diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index 2dedaf9dff80..1055372b660d 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -379,6 +379,16 @@ services: networks: rocketmq_net: + # Open Policy Agent + opa: + image: openpolicyagent/opa:0.35.0 + restart: unless-stopped + ports: + - 8181:8181 + command: run -s + networks: + opa_net: + networks: apisix_net: consul_net: @@ -386,3 +396,4 @@ networks: nacos_net: skywalk_net: rocketmq_net: + opa_net: diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 84fe59b41d14..0a9dec0e74cd 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -331,6 +331,7 @@ plugins: # plugin list (sorted by priority) - jwt-auth # priority: 2510 - key-auth # priority: 2500 - consumer-restriction # priority: 2400 + - opa # priority: 2001 - authz-keycloak # priority: 2000 #- error-log-logger # priority: 1091 - proxy-mirror # priority: 1010 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 4bc6c0d1ee48..c0806346528c 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -83,6 +83,7 @@ basic-auth jwt-auth key-auth consumer-restriction +opa authz-keycloak proxy-mirror proxy-cache diff --git a/t/core/request.t b/t/core/request.t index feb7ac5afde9..c12f2f702cc7 100644 --- a/t/core/request.t +++ b/t/core/request.t @@ -464,3 +464,30 @@ POST /hello POST --- no_error_log [error] + + + +=== TEST 14: get_path +--- config + location /hello1 { + content_by_lua_block { + local core = require("apisix.core") + local ngx_ctx = ngx.ctx + local api_ctx = ngx_ctx.api_ctx + if api_ctx == nil then + api_ctx = core.tablepool.fetch("api_ctx", 0, 32) + ngx_ctx.api_ctx = api_ctx + end + + core.ctx.set_vars_meta(api_ctx) + + local path = core.request.get_path(ngx.ctx.api_ctx) + ngx.say(path) + } + } +--- request +GET /hello1/test?a=b&b=a +--- response_body +/hello1/test +--- no_error_log +[error] diff --git a/t/plugin/opa.t b/t/plugin/opa.t new file mode 100644 index 000000000000..3592f68c6a50 --- /dev/null +++ b/t/plugin/opa.t @@ -0,0 +1,113 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local test_cases = { + {host = "http://127.0.0.1:8181", policy = "example/allow"}, + {host = "http://127.0.0.1:8181"}, + {host = 3233, policy = "example/allow"}, + } + local plugin = require("apisix.plugins.opa") + + for _, case in ipairs(test_cases) do + local ok, err = plugin.check_schema(case) + ngx.say(ok and "done" or err) + end + } + } +--- response_body +done +property "policy" is required +property "host" validation failed: wrong type: expected string, got number + + + +=== TEST 2: setup route with plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "opa": { + "host": "http://127.0.0.1:8181", + "policy": "example/allow" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: hit route (with wrong header request) +--- request +GET /hello +--- more_headers +test-header: not-for-test +--- error_code: 403 + + + +=== TEST 4: hit route (with correct request) +--- request +GET /hello +--- more_headers +test-header: only-for-test +--- response_body +hello world From 8e6372ced6f5a2d8afb5d6393a9f49f23aa4a014 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Dec 2021 09:02:06 +0800 Subject: [PATCH 184/260] chore(deps): bump actions/stale from 4.0.0 to 4.1.0 (#5781) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 984fb8c047b7..ae28da6998bb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Prune Stale - uses: actions/stale@v4.0.0 + uses: actions/stale@v4.1.0 with: days-before-issue-stale: 350 days-before-issue-close: 14 From 34ab8c6218e8a5679b58eb9473744f5216492ec2 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Tue, 14 Dec 2021 06:33:12 +0530 Subject: [PATCH 185/260] docs: improve stream proxy filtering with example (#5783) --- docs/en/latest/admin-api.md | 10 +++-- docs/en/latest/stream-proxy.md | 71 +++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index 20c09334636d..b0fa7f93dbeb 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -977,11 +977,13 @@ By default, this API only returns the http plugins. If you need stream plugins, | Parameter | Required | Type | Description | Example | | ---------------- | ------| -------- | ------| -----| -| remote_addr | False | IP/CIDR | client IP | "127.0.0.1/32" or "127.0.0.1" | -| server_addr | False | IP/CIDR | server IP | "127.0.0.1/32" or "127.0.0.1" | -| server_port | False | Integer | server port | 9090 | -| sni | False | Host | server name indication | "test.com" | | upstream | False | Upstream | Upstream configuration, see [Upstream](architecture-design/upstream.md) for more details | | | upstream_id | False | Upstream | specify the upstream id, see [Upstream](architecture-design/upstream.md) for more details | | +| remote_addr | False | IP/CIDR | Filter option: forward to upstream if client IP matches | "127.0.0.1/32" or "127.0.0.1" | +| server_addr | False | IP/CIDR | Filter option: forward to upstream if APISIX server IP matches with server_addr | "127.0.0.1/32" or "127.0.0.1" | +| server_port | False | Integer | Filter option: forward to upstream if APISIX server port matches with server_port | 9090 | +| sni | False | Host | server name indication | "test.com" | + +To know more about how the filter works, see the documentation [here](./stream-proxy.md#more-route-match-options) [Back to TOC](#table-of-contents) diff --git a/docs/en/latest/stream-proxy.md b/docs/en/latest/stream-proxy.md index 291b9c45a26b..4b32a001c121 100644 --- a/docs/en/latest/stream-proxy.md +++ b/docs/en/latest/stream-proxy.md @@ -74,7 +74,11 @@ For more use cases, please take a look at [test case](https://github.com/apache/ ## More route match options -And we can add more options to match a route. +And we can add more options to match a route. Currently stream route configuration supports 3 fields for filtering: + +- server_addr: The address of the APISIX server that accepts the L4 stream connection. +- server_port: The port of the APISIX server that accepts the L4 stream connection. +- remote_addr: The address of client from which the request has been made. Here is an example: @@ -92,7 +96,70 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 }' ``` -It means APISIX will proxy the request to `127.0.0.1:1995` which the server address is `127.0.0.1` and the server port is equal to `2000`. +It means APISIX will proxy the request to `127.0.0.1:1995` when the server address is `127.0.0.1` and the server port is equal to `2000`. + +Let's take another real world example: + +1. Put this config inside `config.yaml` + + ```yaml + apisix: + stream_proxy: # TCP/UDP proxy + tcp: # TCP proxy address list + - 9100 # by default uses 0.0.0.0 + - "127.0.0.10:9101" + ``` + +2. Now run a mysql docker container and expose port 3306 to the host + + ```shell + $ docker run --name mysql -e MYSQL_ROOT_PASSWORD=toor -p 3306:3306 -d mysql + # check it using a mysql client that it works + $ mysql --host=127.0.0.1 --port=3306 -u root -p + Enter password: + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 25 + ... + mysql> + ``` + +3. Now we are going to create a stream route with server filtering: + + ```shell + curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' + { + "server_addr": "127.0.0.10", + "server_port": 9101, + "upstream": { + "nodes": { + "127.0.0.1:3306": 1 + }, + "type": "roundrobin" + } + }' + ``` + + It only forwards the request to the mysql upstream whenever a connection is received at APISIX server `127.0.0.10` and port `9101`. Let's test that behaviour: + +4. Making a request to 9100 (stream proxy port enabled inside config.yaml), filter matching fails. + + ```shell + $ mysql --host=127.0.0.1 --port=9100 -u root -p + Enter password: + ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 2 + + ``` + + Instead making a request to the APISIX host and port where the filter matching succeeds: + + ```shell + mysql --host=127.0.0.10 --port=9101 -u root -p + Enter password: + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 26 + ... + mysql> + ``` Read [Admin API's Stream Route section](./admin-api.md#stream-route) for the complete options list. From 3c6eb07633b1da0390f706deec19e69675127650 Mon Sep 17 00:00:00 2001 From: Daniel Kocot Date: Tue, 14 Dec 2021 03:11:59 +0100 Subject: [PATCH 186/260] docs: added only: false to example (#5787) --- docs/en/latest/plugins/mqtt-proxy.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/latest/plugins/mqtt-proxy.md b/docs/en/latest/plugins/mqtt-proxy.md index 93d0ef7909cc..4ef020bc6704 100644 --- a/docs/en/latest/plugins/mqtt-proxy.md +++ b/docs/en/latest/plugins/mqtt-proxy.md @@ -57,6 +57,7 @@ For example, the following configuration represents listening on the 9100 TCP po http: 'radixtree_uri' ssl: 'radixtree_sni' stream_proxy: # TCP/UDP proxy + only: false # needed if HTTP and Stream Proxy should be enabled tcp: # TCP proxy port list - 9100 dns_resolver: From 29a3b2afa2fe6d52d1aee38b01d6e7d160b72114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 14 Dec 2021 10:16:05 +0800 Subject: [PATCH 187/260] refactor: use manager to manage batch processor (#5763) --- apisix/plugins/datadog.lua | 61 +--------- apisix/plugins/google-cloud-logging.lua | 89 ++------------ apisix/plugins/http-logger.lua | 66 +---------- apisix/plugins/kafka-logger.lua | 62 +--------- apisix/plugins/rocketmq-logger.lua | 62 +--------- apisix/plugins/skywalking-logger.lua | 67 +---------- apisix/plugins/sls-logger.lua | 63 ++-------- apisix/plugins/syslog.lua | 85 ++++---------- apisix/plugins/tcp-logger.lua | 66 ++--------- apisix/plugins/udp-logger.lua | 63 +--------- apisix/utils/batch-processor-manager.lua | 127 ++++++++++++++++++++ apisix/utils/batch-processor.lua | 1 + docs/en/latest/batch-processor.md | 102 +++++++++++++--- docs/zh/latest/batch-processor.md | 101 +++++++++++++--- t/plugin/error-log-logger-skywalking.t | 2 +- t/plugin/http-logger2.t | 143 +++++++++++++++++++++++ 16 files changed, 518 insertions(+), 642 deletions(-) create mode 100644 apisix/utils/batch-processor-manager.lua create mode 100644 t/plugin/http-logger2.t diff --git a/apisix/plugins/datadog.lua b/apisix/plugins/datadog.lua index b613bac96bc1..8c7062f46592 100644 --- a/apisix/plugins/datadog.lua +++ b/apisix/plugins/datadog.lua @@ -16,7 +16,7 @@ local core = require("apisix.core") local plugin = require("apisix.plugin") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local fetch_log = require("apisix.utils.log-util").get_full_log local latency_details = require("apisix.utils.log-util").latency_details_in_ms local service_fetch = require("apisix.http.service").get @@ -24,12 +24,8 @@ local ngx = ngx local udp = ngx.socket.udp local format = string.format local concat = table.concat -local buffers = {} local ipairs = ipairs -local pairs = pairs local tostring = tostring -local stale_timer_running = false -local timer_at = ngx.timer.at local plugin_name = "datadog" local defaults = { @@ -39,13 +35,12 @@ local defaults = { constant_tags = {"source:apisix"} } +local batch_processor_manager = bp_manager_mod.new(plugin_name) local schema = { type = "object", properties = { - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 5000}, max_retry_count = {type = "integer", minimum = 1, default = 1}, + batch_max_size = {type = "integer", minimum = 1, default = 5000}, prefer_name = {type = "boolean", default = true} } } @@ -68,7 +63,7 @@ local _M = { version = 0.1, priority = 495, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), metadata_schema = metadata_schema, } @@ -115,31 +110,8 @@ local function generate_tag(entry, const_tags) return "" end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end function _M.log(conf, ctx) - - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - local entry = fetch_log(ngx, {}) entry.latency, entry.upstream_latency, entry.apisix_latency = latency_details(ctx) entry.balancer_ip = ctx.balancer_ip or "" @@ -160,9 +132,7 @@ function _M.log(conf, ctx) end end - local log_buffer = buffers[conf] - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -261,27 +231,8 @@ function _M.log(conf, ctx) -- Returning at the end and ensuring the resource has been released. return true end - local config = { - name = plugin_name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end return _M diff --git a/apisix/plugins/google-cloud-logging.lua b/apisix/plugins/google-cloud-logging.lua index a233d3f5ee8d..0b34e2223031 100644 --- a/apisix/plugins/google-cloud-logging.lua +++ b/apisix/plugins/google-cloud-logging.lua @@ -18,20 +18,17 @@ local core = require("apisix.core") local ngx = ngx local tostring = tostring -local pairs = pairs -local ngx_timer_at = ngx.timer.at local http = require("resty.http") local log_util = require("apisix.utils.log-util") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local google_oauth = require("apisix.plugins.google-cloud-logging.oauth") -local buffers = {} local auth_config_cache -local stale_timer_running local plugin_name = "google-cloud-logging" +local batch_processor_manager = bp_manager_mod.new(plugin_name) local schema = { type = "object", properties = { @@ -89,21 +86,6 @@ local schema = { type = "string", default = "apisix.apache.org%2Flogs" }, - max_retry_count = { - type = "integer", - minimum = 0, - default = 0 - }, - retry_delay = { - type = "integer", - minimum = 0, - default = 1 - }, - buffer_duration = { - type = "integer", - minimum = 1, - default = 60 - }, inactive_timeout = { type = "integer", minimum = 1, @@ -122,23 +104,6 @@ local schema = { } --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, route id:", tostring(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - - local function send_to_google(oauth, entries) local http_new = http.new() local access_token = oauth:generate_access_token() @@ -201,34 +166,6 @@ local function get_auth_config(config) end -local function get_logger_buffer(conf, ctx) - local oauth_client = google_oauth:new(auth_config_cache) - - local process = function(entries) - return send_to_google(oauth_client, entries) - end - - local config = { - name = conf.name or plugin_name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local buffer, err = batch_processor:new(process, config) - - if not buffer then - return nil, "error when creating the batch processor: " .. err - end - - return buffer -end - - local function get_logger_entry(conf, ctx) local auth_config, err = get_auth_config(conf) if err or not auth_config.project_id or not auth_config.private_key then @@ -270,7 +207,7 @@ local _M = { version = 0.1, priority = 407, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), } @@ -286,27 +223,17 @@ function _M.log(conf, ctx) return end - if not stale_timer_running then - -- run the timer every 15 minutes if any log is present - ngx_timer_at(900, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end - log_buffer, err = get_logger_buffer(conf, ctx) + local oauth_client = google_oauth:new(auth_config_cache) - if err then - core.log.error(err) - return + local process = function(entries) + return send_to_google(oauth_client, entries) end - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, process) end diff --git a/apisix/plugins/http-logger.lua b/apisix/plugins/http-logger.lua index a4eadb3f0857..d796eea2e08c 100644 --- a/apisix/plugins/http-logger.lua +++ b/apisix/plugins/http-logger.lua @@ -15,7 +15,7 @@ -- limitations under the License. -- -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local log_util = require("apisix.utils.log-util") local core = require("apisix.core") local http = require("resty.http") @@ -25,12 +25,9 @@ local plugin = require("apisix.plugin") local ngx = ngx local tostring = tostring local ipairs = ipairs -local pairs = pairs -local timer_at = ngx.timer.at local plugin_name = "http-logger" -local stale_timer_running = false -local buffers = {} +local batch_processor_manager = bp_manager_mod.new("http logger") local schema = { type = "object", @@ -38,12 +35,6 @@ local schema = { uri = core.schema.uri_def, auth_header = {type = "string", default = ""}, timeout = {type = "integer", minimum = 1, default = 3}, - name = {type = "string", default = "http logger"}, - max_retry_count = {type = "integer", minimum = 0, default = 0}, - retry_delay = {type = "integer", minimum = 0, default = 1}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false}, include_resp_body = {type = "boolean", default = false}, include_resp_body_expr = { @@ -75,7 +66,7 @@ local _M = { version = 0.1, priority = 410, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), metadata_schema = metadata_schema, } @@ -161,24 +152,6 @@ local function send_http_data(conf, log_message) end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - - function _M.body_filter(conf, ctx) log_util.collect_body(conf, ctx) end @@ -202,16 +175,7 @@ function _M.log(conf, ctx) entry.route_id = "no-matched" end - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -253,27 +217,7 @@ function _M.log(conf, ctx) return send_http_data(conf, data) end - local config = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua index 01439225575b..5b6e90380881 100644 --- a/apisix/plugins/kafka-logger.lua +++ b/apisix/plugins/kafka-logger.lua @@ -17,17 +17,15 @@ local core = require("apisix.core") local log_util = require("apisix.utils.log-util") local producer = require ("resty.kafka.producer") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local plugin = require("apisix.plugin") local math = math local pairs = pairs local type = type local plugin_name = "kafka-logger" -local stale_timer_running = false -local timer_at = ngx.timer.at +local batch_processor_manager = bp_manager_mod.new("kafka logger") local ngx = ngx -local buffers = {} local lrucache = core.lrucache.new({ type = "plugin", @@ -66,12 +64,6 @@ local schema = { }, key = {type = "string"}, timeout = {type = "integer", minimum = 1, default = 3}, - name = {type = "string", default = "kafka logger"}, - max_retry_count = {type = "integer", minimum = 0, default = 0}, - retry_delay = {type = "integer", minimum = 0, default = 1}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false}, include_req_body_expr = { type = "array", @@ -112,7 +104,7 @@ local _M = { version = 0.1, priority = 403, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), metadata_schema = metadata_schema, } @@ -157,24 +149,6 @@ local function get_partition_id(prod, topic, log_message) end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - - local function create_producer(broker_list, broker_config, cluster_name) core.log.info("create new kafka producer instance") return producer:new(broker_list, broker_config, cluster_name) @@ -221,15 +195,7 @@ function _M.log(conf, ctx) end end - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -277,25 +243,7 @@ function _M.log(conf, ctx) return send_kafka_data(conf, data, prod) end - local config = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end diff --git a/apisix/plugins/rocketmq-logger.lua b/apisix/plugins/rocketmq-logger.lua index bf7adc5724e1..247c84888bdd 100644 --- a/apisix/plugins/rocketmq-logger.lua +++ b/apisix/plugins/rocketmq-logger.lua @@ -18,16 +18,13 @@ local core = require("apisix.core") local log_util = require("apisix.utils.log-util") local producer = require ("resty.rocketmq.producer") local acl_rpchook = require("resty.rocketmq.acl_rpchook") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local plugin = require("apisix.plugin") local type = type -local pairs = pairs local plugin_name = "rocketmq-logger" -local stale_timer_running = false +local batch_processor_manager = bp_manager_mod.new("rocketmq logger") local ngx = ngx -local timer_at = ngx.timer.at -local buffers = {} local lrucache = core.lrucache.new({ type = "plugin", @@ -55,11 +52,6 @@ local schema = { use_tls = {type = "boolean", default = false}, access_key = {type = "string", default = ""}, secret_key = {type = "string", default = ""}, - name = {type = "string", default = "rocketmq logger"}, - max_retry_count = {type = "integer", minimum = 0, default = 0}, - retry_delay = {type = "integer", minimum = 0, default = 1}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, include_req_body = {type = "boolean", default = false}, include_req_body_expr = { type = "array", @@ -97,7 +89,7 @@ local _M = { version = 0.1, priority = 402, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), metadata_schema = metadata_schema, } @@ -115,24 +107,6 @@ function _M.check_schema(conf, schema_type) end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - - local function create_producer(nameserver_list, producer_config) core.log.info("create new rocketmq producer instance") local prod = producer.new(nameserver_list, "apisixLogProducer") @@ -184,15 +158,7 @@ function _M.log(conf, ctx) end end - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -231,25 +197,7 @@ function _M.log(conf, ctx) return send_rocketmq_data(conf, data, prod) end - local config = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end diff --git a/apisix/plugins/skywalking-logger.lua b/apisix/plugins/skywalking-logger.lua index bd5f9c1169eb..7258cb3c26b7 100644 --- a/apisix/plugins/skywalking-logger.lua +++ b/apisix/plugins/skywalking-logger.lua @@ -15,7 +15,7 @@ -- limitations under the License. -- -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local log_util = require("apisix.utils.log-util") local core = require("apisix.core") local http = require("resty.http") @@ -28,13 +28,9 @@ local ngx_re = require("ngx.re") local ngx = ngx local tostring = tostring local tonumber = tonumber -local pairs = pairs -local timer_at = ngx.timer.at local plugin_name = "skywalking-logger" -local stale_timer_running = false -local buffers = {} - +local batch_processor_manager = bp_manager_mod.new("skywalking logger") local schema = { type = "object", properties = { @@ -42,12 +38,6 @@ local schema = { service_name = {type = "string", default = "APISIX"}, service_instance_name = {type = "string", default = "APISIX Instance Name"}, timeout = {type = "integer", minimum = 1, default = 3}, - name = {type = "string", default = "skywalking logger"}, - max_retry_count = {type = "integer", minimum = 0, default = 0}, - retry_delay = {type = "integer", minimum = 0, default = 1}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false}, }, required = {"endpoint_addr"}, @@ -66,7 +56,7 @@ local _M = { version = 0.1, priority = 408, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), metadata_schema = metadata_schema, } @@ -124,24 +114,6 @@ local function send_http_data(conf, log_message) end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - - function _M.log(conf, ctx) local metadata = plugin.plugin_metadata(plugin_name) core.log.info("metadata: ", core.json.delay_encode(metadata)) @@ -183,16 +155,7 @@ function _M.log(conf, ctx) endpoint = ctx.var.uri, } - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -206,27 +169,7 @@ function _M.log(conf, ctx) return send_http_data(conf, data) end - local config = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end diff --git a/apisix/plugins/sls-logger.lua b/apisix/plugins/sls-logger.lua index 797b85fd8894..ed34c847ebe2 100644 --- a/apisix/plugins/sls-logger.lua +++ b/apisix/plugins/sls-logger.lua @@ -16,29 +16,22 @@ -- local core = require("apisix.core") local log_util = require("apisix.utils.log-util") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local plugin_name = "sls-logger" local ngx = ngx local rf5424 = require("apisix.plugins.slslog.rfc5424") -local stale_timer_running = false; -local timer_at = ngx.timer.at local tcp = ngx.socket.tcp -local buffers = {} local tostring = tostring local ipairs = ipairs -local pairs = pairs local table = table + + +local batch_processor_manager = bp_manager_mod.new(plugin_name) local schema = { type = "object", properties = { include_req_body = {type = "boolean", default = false}, - name = {type = "string", default = "sls-logger"}, timeout = {type = "integer", minimum = 1, default= 5000}, - max_retry_count = {type = "integer", minimum = 0, default = 0}, - retry_delay = {type = "integer", minimum = 0, default = 1}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, host = {type = "string"}, port = {type = "integer"}, project = {type = "string"}, @@ -53,7 +46,7 @@ local _M = { version = 0.1, priority = 406, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), } function _M.check_schema(conf) @@ -111,22 +104,6 @@ local function send_tcp_data(route_conf, log_message) return res, err_msg end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, route id:", tostring(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - local function combine_syslog(entries) local items = {} for _, entry in ipairs(entries) do @@ -171,37 +148,11 @@ function _M.log(conf, ctx) route_conf = conf } - local log_buffer = buffers[entry.route_id] - if not stale_timer_running then - -- run the timer every 15 mins if any log is present - timer_at(900, remove_stale_objects) - stale_timer_running = true - end - - if log_buffer then - log_buffer:push(process_context) - return - end - - local process_conf = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - log_buffer, err = batch_processor:new(handle_log, process_conf) - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) + if batch_processor_manager:add_entry(conf, process_context) then return end - buffers[entry.route_id] = log_buffer - log_buffer:push(process_context) + batch_processor_manager:add_entry_to_new_processor(conf, process_context, ctx, handle_log) end diff --git a/apisix/plugins/syslog.lua b/apisix/plugins/syslog.lua index 3eed59ff433f..65b101b5b3ee 100644 --- a/apisix/plugins/syslog.lua +++ b/apisix/plugins/syslog.lua @@ -17,32 +17,26 @@ local core = require("apisix.core") local log_util = require("apisix.utils.log-util") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local logger_socket = require("resty.logger.socket") local plugin_name = "syslog" local ngx = ngx -local buffers = {} -local pairs = pairs -local stale_timer_running = false; -local timer_at = ngx.timer.at +local batch_processor_manager = bp_manager_mod.new("sys logger") local schema = { type = "object", properties = { host = {type = "string"}, port = {type = "integer"}, - name = {type = "string", default = "sys logger"}, + max_retry_times = {type = "integer", minimum = 1, default = 1}, + retry_interval = {type = "integer", minimum = 0, default = 1}, flush_limit = {type = "integer", minimum = 1, default = 4096}, drop_limit = {type = "integer", default = 1048576}, timeout = {type = "integer", minimum = 1, default = 3}, sock_type = {type = "string", default = "tcp", enum = {"tcp", "udp"}}, - max_retry_times = {type = "integer", minimum = 1, default = 1}, - retry_interval = {type = "integer", minimum = 0, default = 1}, pool_size = {type = "integer", minimum = 5, default = 5}, tls = {type = "boolean", default = false}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, include_req_body = {type = "boolean", default = false} }, required = {"host", "port"} @@ -54,6 +48,13 @@ local lrucache = core.lrucache.new({ }) +-- syslog uses max_retry_times/retry_interval/timeout +-- instead of max_retry_count/retry_delay/inactive_timeout +local schema = batch_processor_manager:wrap_schema(schema) +schema.max_retry_count = nil +schema.retry_delay = nil +schema.inactive_timeout = nil + local _M = { version = 0.1, priority = 401, @@ -63,7 +64,17 @@ local _M = { function _M.check_schema(conf) - return core.schema.check(schema, conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + -- syslog uses max_retry_times/retry_interval/timeout + -- instead of max_retry_count/retry_delay/inactive_timeout + conf.max_retry_count = conf.max_retry_times + conf.retry_delay = conf.retry_interval + conf.inactive_timeout = conf.timeout + return true end @@ -115,38 +126,11 @@ local function send_syslog_data(conf, log_message, api_ctx) end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - - -- log phase in APISIX function _M.log(conf, ctx) local entry = log_util.get_full_log(ngx, conf) - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -167,28 +151,7 @@ function _M.log(conf, ctx) return send_syslog_data(conf, data, cp_ctx) end - local config = { - name = conf.name, - retry_delay = conf.retry_interval, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_times, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) - + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end diff --git a/apisix/plugins/tcp-logger.lua b/apisix/plugins/tcp-logger.lua index 8d678b33ca1b..651ab03ba94d 100644 --- a/apisix/plugins/tcp-logger.lua +++ b/apisix/plugins/tcp-logger.lua @@ -16,16 +16,14 @@ -- local core = require("apisix.core") local log_util = require("apisix.utils.log-util") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local plugin_name = "tcp-logger" local tostring = tostring -local buffers = {} local ngx = ngx local tcp = ngx.socket.tcp -local pairs = pairs -local stale_timer_running = false -local timer_at = ngx.timer.at + +local batch_processor_manager = bp_manager_mod.new("tcp logger") local schema = { type = "object", properties = { @@ -34,12 +32,6 @@ local schema = { tls = {type = "boolean", default = false}, tls_options = {type = "string"}, timeout = {type = "integer", minimum = 1, default= 1000}, - name = {type = "string", default = "tcp logger"}, - max_retry_count = {type = "integer", minimum = 0, default = 0}, - retry_delay = {type = "integer", minimum = 0, default = 1}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false} }, required = {"host", "port"} @@ -50,7 +42,7 @@ local _M = { version = 0.1, priority = 405, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), } function _M.check_schema(conf) @@ -100,36 +92,11 @@ local function send_tcp_data(conf, log_message) return res, err_msg end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - function _M.log(conf, ctx) local entry = log_util.get_full_log(ngx, conf) - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -149,27 +116,8 @@ function _M.log(conf, ctx) return send_tcp_data(conf, data) end - local config = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end + return _M diff --git a/apisix/plugins/udp-logger.lua b/apisix/plugins/udp-logger.lua index 97e6552cfd65..996295289879 100644 --- a/apisix/plugins/udp-logger.lua +++ b/apisix/plugins/udp-logger.lua @@ -16,26 +16,20 @@ -- local core = require("apisix.core") local log_util = require("apisix.utils.log-util") -local batch_processor = require("apisix.utils.batch-processor") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") local plugin_name = "udp-logger" local tostring = tostring -local buffers = {} local ngx = ngx local udp = ngx.socket.udp -local pairs = pairs -local stale_timer_running = false; -local timer_at = ngx.timer.at + +local batch_processor_manager = bp_manager_mod.new("udp logger") local schema = { type = "object", properties = { host = {type = "string"}, port = {type = "integer", minimum = 0}, timeout = {type = "integer", minimum = 1, default = 3}, - name = {type = "string", default = "udp logger"}, - buffer_duration = {type = "integer", minimum = 1, default = 60}, - inactive_timeout = {type = "integer", minimum = 1, default = 5}, - batch_max_size = {type = "integer", minimum = 1, default = 1000}, include_req_body = {type = "boolean", default = false} }, required = {"host", "port"} @@ -46,7 +40,7 @@ local _M = { version = 0.1, priority = 400, name = plugin_name, - schema = schema, + schema = batch_processor_manager:wrap_schema(schema), } function _M.check_schema(conf) @@ -84,36 +78,11 @@ local function send_udp_data(conf, log_message) return res, err_msg end --- remove stale objects from the memory after timer expires -local function remove_stale_objects(premature) - if premature then - return - end - - for key, batch in pairs(buffers) do - if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then - core.log.warn("removing batch processor stale object, conf: ", - core.json.delay_encode(key)) - buffers[key] = nil - end - end - - stale_timer_running = false -end - function _M.log(conf, ctx) local entry = log_util.get_full_log(ngx, conf) - if not stale_timer_running then - -- run the timer every 30 mins if any log is present - timer_at(1800, remove_stale_objects) - stale_timer_running = true - end - - local log_buffer = buffers[conf] - if log_buffer then - log_buffer:push(entry) + if batch_processor_manager:add_entry(conf, entry) then return end @@ -133,27 +102,7 @@ function _M.log(conf, ctx) return send_udp_data(conf, data) end - local config = { - name = conf.name, - retry_delay = conf.retry_delay, - batch_max_size = conf.batch_max_size, - max_retry_count = conf.max_retry_count, - buffer_duration = conf.buffer_duration, - inactive_timeout = conf.inactive_timeout, - route_id = ctx.var.route_id, - server_addr = ctx.var.server_addr, - } - - local err - log_buffer, err = batch_processor:new(func, config) - - if not log_buffer then - core.log.error("error when creating the batch processor: ", err) - return - end - - buffers[conf] = log_buffer - log_buffer:push(entry) + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) end return _M diff --git a/apisix/utils/batch-processor-manager.lua b/apisix/utils/batch-processor-manager.lua new file mode 100644 index 000000000000..4e7830d831e1 --- /dev/null +++ b/apisix/utils/batch-processor-manager.lua @@ -0,0 +1,127 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +local core = require("apisix.core") +local batch_processor = require("apisix.utils.batch-processor") +local timer_at = ngx.timer.at +local pairs = pairs +local setmetatable = setmetatable + + +local _M = {} +local mt = { __index = _M } + + +function _M.new(name) + return setmetatable({ + stale_timer_running = false, + buffers = {}, + name = name, + }, mt) +end + + +function _M:wrap_schema(schema) + local bp_schema = core.table.deepcopy(batch_processor.schema) + local properties = schema.properties + for k, v in pairs(bp_schema.properties) do + if not properties[k] then + properties[k] = v + end + -- don't touch if the plugin overrides the property + end + + properties.name.default = self.name + return schema +end + + +-- remove stale objects from the memory after timer expires +local function remove_stale_objects(premature, self) + if premature then + return + end + + for key, batch in pairs(self.buffers) do + if #batch.entry_buffer.entries == 0 and #batch.batch_to_process == 0 then + core.log.warn("removing batch processor stale object, conf: ", + core.json.delay_encode(key)) + self.buffers[key] = nil + end + end + + self.stale_timer_running = false +end + + +local check_stale +do + local interval = 1800 + + function check_stale(self) + if not self.stale_timer_running then + -- run the timer every 30 mins if any log is present + timer_at(interval, remove_stale_objects, self) + self.stale_timer_running = true + end + end + + function _M.set_check_stale_interval(time) + interval = time + end +end + + +function _M:add_entry(conf, entry) + check_stale(self) + + local log_buffer = self.buffers[conf] + if not log_buffer then + return false + end + + log_buffer:push(entry) + return true +end + + +function _M:add_entry_to_new_processor(conf, entry, ctx, func) + check_stale(self) + + local config = { + name = conf.name, + batch_max_size = conf.batch_max_size, + max_retry_count = conf.max_retry_count, + retry_delay = conf.retry_delay, + buffer_duration = conf.buffer_duration, + inactive_timeout = conf.inactive_timeout, + route_id = ctx.var.route_id, + server_addr = ctx.var.server_addr, + } + + local log_buffer, err = batch_processor:new(func, config) + if not log_buffer then + core.log.error("error when creating the batch processor: ", err) + return false + end + + log_buffer:push(entry) + self.buffers[conf] = log_buffer + return true +end + + +return _M diff --git a/apisix/utils/batch-processor.lua b/apisix/utils/batch-processor.lua index 0de2fc6a4533..b94a0dd4f1e3 100644 --- a/apisix/utils/batch-processor.lua +++ b/apisix/utils/batch-processor.lua @@ -42,6 +42,7 @@ local schema = { batch_max_size = {type = "integer", minimum = 1, default= 1000}, } } +batch_processor.schema = schema local function schedule_func_exec(self, delay, batch) diff --git a/docs/en/latest/batch-processor.md b/docs/en/latest/batch-processor.md index ef08db89435f..08ec5dc80586 100644 --- a/docs/en/latest/batch-processor.md +++ b/docs/en/latest/batch-processor.md @@ -39,31 +39,97 @@ or when the buffer duration exceeds. |max_retry_count|optional |Maximum number of retries before removing from the processing pipe line; default is zero| |retry_delay |optional |Number of seconds the process execution should be delayed if the execution fails; default is 1| -The following code shows an example of how to use a batch processor. The batch processor takes a function to be executed as the first -argument and the batch configuration as the second parameter. +The following code shows an example of how to use batch processor in your plugin: ```lua -local bp = require("apisix.utils.batch-processor") -local func_to_execute = function(entries) - -- serialize to json array core.json.encode(entries) - -- process/send data - return true - end - -local config = { - max_retry_count = 2, - buffer_duration = 60, - inactive_timeout = 5, - batch_max_size = 1, - retry_delay = 0 +local bp_manager_mod = require("apisix.utils.batch-processor-manager") +... + +local plugin_name = "xxx-logger" +local batch_processor_manager = bp_manager_mod.new(plugin_name) +local schema = {...} +local _M = { + ... + name = plugin_name, + schema = batch_processor_manager:wrap_schema(schema), } +... + + +function _M.log(conf, ctx) + local entry = {...} -- data to log + + if batch_processor_manager:add_entry(conf, entry) then + return + end + -- create a new processor if not found + + -- entries is an array table of entry, which can be processed in batch + local func = function(entries) + -- serialize to json array core.json.encode(entries) + -- process/send data + return true + -- return false, err_msg if failed + end + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) +end +``` + +The batch processor's configuration will be set inside the plugin's configuration. +For example: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "http-logger": { + "uri": "http://mockbin.org/bin/:ID", + "batch_max_size": 10, + "max_retry_count": 1 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` -local batch_processor, err = bp:new(func_to_execute, config) +If your plugin only uses one global batch processor, +you can also use the processor directly: -if batch_processor then - batch_processor:push({hello='world'}) +```lua +local entry = {...} -- data to log +if log_buffer then + log_buffer:push(entry) + return end + +local config_bat = { + name = config.name, + retry_delay = config.retry_delay, + ... +} + +local err +-- entries is an array table of entry, which can be processed in batch +local func = function(entries) + ... + return true + -- return false, err_msg if failed +end +log_buffer, err = batch_processor:new(func, config_bat) + +if not log_buffer then + core.log.warn("error when creating the batch processor: ", err) + return +end + +log_buffer:push(entry) ``` Note: Please make sure the batch max size (entry count) is within the limits of the function execution. diff --git a/docs/zh/latest/batch-processor.md b/docs/zh/latest/batch-processor.md index 95d8fe78eba9..78f755cd88c4 100644 --- a/docs/zh/latest/batch-processor.md +++ b/docs/zh/latest/batch-processor.md @@ -37,30 +37,97 @@ title: 批处理器 |max_retry_count|可选的 |从处理管道中移除之前的最大重试次数;默认为 `0`| |retry_delay |可选的 |如果执行失败,应该延迟进程执行的秒数;默认为 `1`| -以下代码显示了如何使用批处理程序的示例。批处理器将一个要执行的函数作为第一个参数,将批处理配置作为第二个参数。 +以下代码显示了如何在你的插件中使用批处理器: ```lua -local bp = require("apisix.utils.batch-processor") -local func_to_execute = function(entries) - -- serialize to json array core.json.encode(entries) - -- process/send data - return true - end - -local config = { - max_retry_count = 2, - buffer_duration = 60, - inactive_timeout = 5, - batch_max_size = 1, - retry_delay = 0 +local bp_manager_mod = require("apisix.utils.batch-processor-manager") +... + +local plugin_name = "xxx-logger" +local batch_processor_manager = bp_manager_mod.new(plugin_name) +local schema = {...} +local _M = { + ... + name = plugin_name, + schema = batch_processor_manager:wrap_schema(schema), } +... + + +function _M.log(conf, ctx) + local entry = {...} -- data to log + + if batch_processor_manager:add_entry(conf, entry) then + return + end + -- create a new processor if not found + + -- entries is an array table of entry, which can be processed in batch + local func = function(entries) + -- serialize to json array core.json.encode(entries) + -- process/send data + return true + -- return false, err_msg if failed + end + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, func) +end +``` + +批处理器的配置将通过该插件的配置设置。 +举个例子: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "http-logger": { + "uri": "http://mockbin.org/bin/:ID", + "batch_max_size": 10, + "max_retry_count": 1 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/hello" +}' +``` -local batch_processor, err = bp:new(func_to_execute, config) +如果你的插件只使用一个全局的批处理器, +你可以直接使用它: -if batch_processor then - batch_processor:push({hello='world'}) +```lua +local entry = {...} -- data to log +if log_buffer then + log_buffer:push(entry) + return end + +local config_bat = { + name = config.name, + retry_delay = config.retry_delay, + ... +} + +local err +-- entries is an array table of entry, which can be processed in batch +local func = function(entries) + ... + return true + -- return false, err_msg if failed +end +log_buffer, err = batch_processor:new(func, config_bat) + +if not log_buffer then + core.log.warn("error when creating the batch processor: ", err) + return +end + +log_buffer:push(entry) ``` 注意:请确保批处理的最大值(条目数)在函数执行的范围内。 diff --git a/t/plugin/error-log-logger-skywalking.t b/t/plugin/error-log-logger-skywalking.t index 0e5f5dce4f5f..289ac369e947 100644 --- a/t/plugin/error-log-logger-skywalking.t +++ b/t/plugin/error-log-logger-skywalking.t @@ -84,7 +84,7 @@ plugins: GET /tg --- response_body --- error_log eval -qr/.*\[lua\] batch-processor.lua:63: Batch Processor\[error-log-logger\] failed to process entries: error while sending data to skywalking\[http:\/\/127.0.0.1:1988\/log\] connection refused, context: ngx.timer/ +qr/Batch Processor\[error-log-logger\] failed to process entries: error while sending data to skywalking\[http:\/\/127.0.0.1:1988\/log\] connection refused, context: ngx.timer/ --- wait: 3 diff --git a/t/plugin/http-logger2.t b/t/plugin/http-logger2.t new file mode 100644 index 000000000000..ae16f6b8c119 --- /dev/null +++ b/t/plugin/http-logger2.t @@ -0,0 +1,143 @@ +# +# 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'; + +log_level('debug'); +repeat_each(1); +no_long_string(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]"); + } + + my $extra_init_by_lua = <<_EOC_; + local bpm = require("apisix.utils.batch-processor-manager") + bpm.set_check_stale_interval(1) +_EOC_ + + $block->set_value("extra_init_by_lua", $extra_init_by_lua); +}); + +run_tests; + +__DATA__ + +=== TEST 1: check stale batch processor +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "http-logger": { + "uri": "http://127.0.0.1:1982/hello", + "batch_max_size": 1 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: don't remove current processor +--- request +GET /opentracing +--- error_log +Batch Processor[http logger] successfully processed the entries +--- no_error_log +removing batch processor stale object +--- wait: 0.5 + + + +=== TEST 3: remove stale processor +--- request +GET /opentracing +--- error_log +Batch Processor[http logger] successfully processed the entries +removing batch processor stale object +--- wait: 1.5 + + + +=== TEST 4: don't remove batch processor which is in used +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "http-logger": { + "uri": "http://127.0.0.1:1982/hello", + "batch_max_size": 2 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1982": 1 + }, + "type": "roundrobin" + }, + "uri": "/opentracing" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: don't remove +--- request +GET /opentracing +--- no_error_log +removing batch processor stale object +--- wait: 1.5 From bb8ff5b1fde853b879c1922675d282a457de080e Mon Sep 17 00:00:00 2001 From: S96EA Date: Tue, 14 Dec 2021 10:59:43 +0800 Subject: [PATCH 188/260] fix(ext-plugin): don't use stale key (#5782) Co-authored-by: sunaowei --- apisix/plugins/ext-plugin/init.lua | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index 772bd3832ed3..2a2aee6f7a38 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -65,11 +65,16 @@ local type = type local events_list -local lrucache = core.lrucache.new({ - type = "plugin", - invalid_stale = true, - ttl = helper.get_conf_token_cache_time(), -}) + +local function new_lrucache() + return core.lrucache.new({ + type = "plugin", + invalid_stale = true, + ttl = helper.get_conf_token_cache_time(), + }) +end +local lrucache = new_lrucache() + local shdict_name = "ext-plugin" local shdict = ngx.shared[shdict_name] @@ -668,17 +673,14 @@ rpc_call = function (ty, conf, ctx, ...) end -local function create_lrucache() +local function recreate_lrucache() flush_token() if lrucache then core.log.warn("flush conf token lrucache") end - lrucache = core.lrucache.new({ - type = "plugin", - ttl = helper.get_conf_token_cache_time(), - }) + lrucache = new_lrucache() end @@ -702,7 +704,7 @@ function _M.communicate(conf, ctx, plugin_name) end core.log.warn("refresh cache and try again") - create_lrucache() + recreate_lrucache() end core.log.error(err) @@ -799,7 +801,7 @@ function _M.init_worker() ) -- flush cache when runner exited - events.register(create_lrucache, events_list._source, events_list.runner_exit) + events.register(recreate_lrucache, events_list._source, events_list.runner_exit) -- note that the runner is run under the same user as the Nginx master if process.type() == "privileged agent" then From 88b00cc6d0e6dc2ee1c9c2de7460c140b0973acd Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Tue, 14 Dec 2021 14:09:25 +0800 Subject: [PATCH 189/260] docs: sync mqtt-proxy change to zh docs (#5794) --- docs/zh/latest/plugins/mqtt-proxy.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/zh/latest/plugins/mqtt-proxy.md b/docs/zh/latest/plugins/mqtt-proxy.md index 3a0aff7ad6c3..a320c92c1aa0 100644 --- a/docs/zh/latest/plugins/mqtt-proxy.md +++ b/docs/zh/latest/plugins/mqtt-proxy.md @@ -55,6 +55,7 @@ title: mqtt-proxy http: 'radixtree_uri' ssl: 'radixtree_sni' stream_proxy: # TCP/UDP proxy + only: false # 如需 HTTP 与 Stream 代理同时生效,需要增加该键值 tcp: # TCP proxy port list - 9100 dns_resolver: From 936d9ac72b722b308a0a0dce01e0784a135d0cef Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Tue, 14 Dec 2021 14:13:31 +0800 Subject: [PATCH 190/260] docs: refactor Installation Guide (#5718) --- docs/en/latest/how-to-build.md | 91 +++++++++++++++++++-------------- docs/zh/latest/how-to-build.md | 93 ++++++++++++++++++++-------------- 2 files changed, 109 insertions(+), 75 deletions(-) diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 96d9f1212938..0991805c5ca9 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -21,36 +21,37 @@ title: How to build Apache APISIX # --> -## Step 1: Install dependencies +## Step 1: Install Apache APISIX -The Apache APISIX runtime environment requires dependencies on NGINX and etcd. +You can install Apache APISIX via RPM Repository, Docker, Helm Chart, and source release package. Please choose one from the following options. -Before installing Apache APISIX, please install dependencies according to the operating system you are using. We provide the dependencies installation instructions for **CentOS7**, **Fedora 31 & 32**, **Ubuntu 16.04 & 18.04**, **Debian 9 & 10**, and **MacOS**, please refer to [Install Dependencies](install-dependencies.md) for more details. +### Installation via RPM Repository(CentOS 7) -## Step 2: Install Apache APISIX +This installation method is suitable for CentOS 7. -You can install Apache APISIX via RPM Repository, RPM package, Docker, Helm Chart, and source release package. Please choose one from the following options. +If the official OpenResty repository is not installed yet, the following command will help you automatically install both the repositories of OpenResty and Apache APISIX. -### Installation via RPM Repository(CentOS 7) +```shell +$ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +``` -This installation method is suitable for CentOS 7. For now, the Apache APISIX RPM repository for CentOS 7 is already supported. Please run the following commands to install the repository and Apache APISIX. +If the official OpenResty repository is installed, the following command will help you automatically install the repositories of Apache APISIX. + +```shell +$ sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo +``` + +Please run the following commands to install the repository and Apache APISIX. ```shell -sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo # View the information of the latest apisix package -sudo yum info -y apisix +$ sudo yum info -y apisix # Will show the existing apisix packages -sudo yum --showduplicates list apisix +$ sudo yum --showduplicates list apisix # Will install the latest apisix package -sudo yum install apisix -``` - -If the official OpenResty repository is not installed yet, the following command will help you automatically install both the repositories of OpenResty and Apache APISIX. - -```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +$ sudo yum install apisix ``` ### Installation via Docker @@ -66,13 +67,14 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a 1. Create a directory named `apisix-2.11.0`. ```shell - mkdir apisix-2.11.0 + $ APISIX_VERSION='2.11.0' + $ mkdir apisix-${APISIX_VERSION} ``` 2. Download Apache APISIX Release source package. ```shell - wget https://downloads.apache.org/apisix/2.11.0/apache-apisix-2.11.0-src.tgz + $ wget https://downloads.apache.org/apisix/${APISIX_VERSION}/apache-apisix-${APISIX_VERSION}-src.tgz ``` You can also download the Apache APISIX Release source package from the Apache APISIX website. The [Apache APISIX Official Website - Download Page](https://apisix.apache.org/downloads/) also provides source packages for Apache APISIX, APISIX Dashboard and APISIX Ingress Controller. @@ -80,20 +82,35 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a 3. Unzip the Apache APISIX Release source package. ```shell - tar zxvf apache-apisix-2.11.0-src.tgz -C apisix-2.11.0 + $ tar zxvf apache-apisix-${APISIX_VERSION}-src.tgz -C apisix-${APISIX_VERSION} ``` 4. Install the runtime dependent Lua libraries. ```shell - # Switch to the apisix-2.11.0 directory - cd apisix-2.11.0 + # Switch to the apisix-${APISIX_VERSION} directory + $ cd apisix-${APISIX_VERSION} # Create dependencies - make deps + $ make depsInstall Apache APISIX # Install apisix command - make install + $ make install ``` +## Step 2: Install ETCD + +This step is required if you have installed only Apache APISIX via RPM, Docker or source code but not ETCD. + +You can install ETCD via Docker or binary etc. The following command installs ETCD via binary. + +```shell +$ ETCD_VERSION='3.4.13' +$ wget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz +$ tar -xvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz && \ + cd etcd-v${ETCD_VERSION}-linux-amd64 && \ + sudo cp -a etcd etcdctl /usr/bin/ +$ nohup etcd & +``` + ## Step 3: Manage Apache APISIX Server We can initialize dependencies, start service, and stop service with commands in the Apache APISIX directory, we can also view all commands and their corresponding functions with the `apisix help` command. @@ -104,7 +121,7 @@ Run the following command to initialize the NGINX configuration file and etcd. ```shell # initialize NGINX config file and etcd -apisix init +$ apisix init ``` ### Test configuration file @@ -113,7 +130,7 @@ Run the following command to test the configuration file. APISIX will generate ` ```shell # generate `nginx.conf` from `config.yaml` and test it -apisix test +$ apisix test ``` ### Start Apache APISIX @@ -122,7 +139,7 @@ Run the following command to start Apache APISIX. ```shell # start Apache APISIX server -apisix start +$ apisix start ``` ### Stop Apache APISIX @@ -135,14 +152,14 @@ The command to perform a graceful shutdown is shown below. ```shell # stop Apache APISIX server gracefully -apisix quit +$ apisix quit ``` The command to perform a forced shutdown is shown below. ```shell # stop Apache APISIX server immediately -apisix stop +$ apisix stop ``` ### View Other Operations @@ -151,7 +168,7 @@ Run the `apisix help` command to see the returned results and get commands and d ```shell # more actions find by `help` -apisix help +$ apisix help ``` ## Step 4: Run Test Cases @@ -161,13 +178,13 @@ apisix help 2. Then install the test-nginx dependencies via `cpanm`: ```shell - sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) + $ sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) ``` 3. Run the `git clone` command to clone the latest source code locally, please use the version we forked out: ```shell - git clone https://github.com/iresty/test-nginx.git + $ git clone https://github.com/iresty/test-nginx.git ``` 4. Here are two ways of running tests: @@ -209,7 +226,7 @@ Ensure that OpenResty is set to the default NGINX, and export the path as follow Run the specified test case using the following command. ```shell -prove -Itest-nginx/lib -r t/plugin/openid-connect.t +$ prove -Itest-nginx/lib -r t/plugin/openid-connect.t ``` For more details on the test cases, see the [testing framwork](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md). @@ -233,7 +250,7 @@ apisix: When we need to access the Admin API, we can use the key above, as shown below. ```shell -curl http://127.0.0.1:9080/apisix/admin/routes?api_key=abcdefghabcdefgh -i +$ curl http://127.0.0.1:9080/apisix/admin/routes?api_key=abcdefghabcdefgh -i ``` The status code 200 in the returned result indicates that the access was successful, as shown below. @@ -249,7 +266,7 @@ Content-Type: text/plain At this point, if the key you enter does not match the value of `apisix.admin_key` in `conf/config.yaml`, for example, we know that the correct key is `abcdefghabcdefgh`, but we enter an incorrect key, such as `wrong-key`, as shown below. ```shell -curl http://127.0.0.1:9080/apisix/admin/routes?api_key=wrong-key -i +$ curl http://127.0.0.1:9080/apisix/admin/routes?api_key=wrong-key -i ``` The status code `401` in the returned result indicates that the access failed because the `key` entered was incorrect and did not pass authentication, triggering an `Unauthorized` error, as shown below. @@ -273,8 +290,8 @@ You can refer to the source of [api7/apisix-build-tools](https://github.com/api7 If you are using CentOS 7 and you installed Apache APISIX via the RPM package in step 2, the configuration file is already in place automatically and you can run the following command directly. ```shell -systemctl start apisix -systemctl stop apisix +$ systemctl start apisix +$ systemctl stop apisix ``` If you installed Apache APISIX by other methods, you can refer to the [configuration file template](https://github.com/api7/apisix-build-tools/blob/master/usr/lib/systemd/system/apisix.service) for modification and put it in the `/usr/lib/systemd/system/apisix.service` path. diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index f997ab17bf3b..6775c89d41aa 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -21,36 +21,37 @@ title: 如何构建 Apache APISIX # --> -## 步骤1:安装依赖 +## 步骤1:安装 Apache APISIX -Apache APISIX 的运行环境需要依赖 NGINX 和 etcd,所以在安装 Apache APISIX 前,请根据您使用的操作系统安装对应的依赖。我们提供了 **CentOS7** 、**Fedora 31 & 32** 、**Ubuntu 16.04 & 18.04** 、 **Debian 9 & 10** 和 **MacOS** 上的依赖安装操作步骤,详情请参考 [安装依赖](install-dependencies.md)。 - -通过 Docker 或 Helm Chart 安装 Apache APISIX 时,已经包含了所需的 NGINX 和 etcd,请参照各自对应的文档。 - -## 步骤2:安装 Apache APISIX - -你可以通过 RPM 仓库、RPM 包、Docker、Helm Chart、源码包等多种方式来安装 Apache APISIX。请在以下选项中选择其中一种执行。 +你可以通过 RPM 仓库、Docker、Helm Chart、源码包等多种方式来安装 Apache APISIX。请在以下选项中选择其中一种执行。 ### 通过 RPM 仓库安装(CentOS 7) -这种安装方式适用于 CentOS 7 操作系统。Apache APISIX 已经支持适用于 CentOS 7 的 RPM 仓库。请运行以下命令安装 RPM 仓库和 Apache APISIX。 +这种安装方式适用于 CentOS 7 操作系统。 + +如果尚未安装 OpenResty 的官方 RPM 仓库,请使用以下命令自动安装 OpenResty 和 Apache APISIX 的 RPM 仓库。 ```shell -sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo -# View the information of the latest apisix package -sudo yum info -y apisix +$ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +``` -# Will show the existing apisix packages -sudo yum --showduplicates list apisix +如果已安装 OpenResty 的官方 RPM 仓库,请使用以下命令自动安装 Apache APISIX 的 RPM 仓库。 -# Will install the latest apisix package -sudo yum install apisix +```shell +$ sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo ``` -如果尚未安装 OpenResty 的官方 RPM 仓库,以下命令可以帮助您自动安装 OpenResty 和 Apache APISIX 的 RPM 仓库。 +请运行以下命令安装 Apache APISIX。 ```shell -sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +# 查看仓库中最新的 apisix 软件包的信息 +$ sudo yum info -y apisix + +# 显示仓库中现有的 apisix 软件包 +$ sudo yum --showduplicates list apisix + +# 安装最新的 apisix 软件包 +$ sudo yum install apisix ``` ### 通过 Docker 安装 @@ -66,13 +67,14 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep 1. 创建一个名为 `apisix-2.11.0` 的目录。 ```shell - mkdir apisix-2.11.0 + $ APISIX_VERSION='2.11.0' + $ mkdir apisix-${APISIX_VERSION} ``` 2. 下载 Apache APISIX Release 源码包: ```shell - wget https://downloads.apache.org/apisix/2.11.0/apache-apisix-2.11.0-src.tgz + $ wget https://downloads.apache.org/apisix/${APISIX_VERSION}/apache-apisix-${APISIX_VERSION}-src.tgz ``` 您也可以通过 Apache APISIX 官网下载 Apache APISIX Release 源码包。 Apache APISIX 官网也提供了 Apache APISIX、APISIX Dashboard 和 APISIX Ingress Controller 的源码包,详情请参考 [Apache APISIX 官网-下载页](https://apisix.apache.org/zh/downloads)。 @@ -80,20 +82,35 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep 3. 解压 Apache APISIX Release 源码包: ```shell - tar zxvf apache-apisix-2.11.0-src.tgz -C apisix-2.11.0 + $ tar zxvf apache-apisix-${APISIX_VERSION}-src.tgz -C apisix-${APISIX_VERSION} ``` 4. 安装运行时依赖的 Lua 库: ```shell - # 切换到 apisix-2.11.0 目录 - cd apisix-2.11.0 + # 切换到 apisix-${APISIX_VERSION} 目录 + $ cd apisix-${APISIX_VERSION} # 安装依赖 - LUAROCKS_SERVER=https://luarocks.cn make deps + $ LUAROCKS_SERVER=https://luarocks.cn make deps # 安装 apisix 命令 - make install + $ make install ``` +## 步骤2:安装 ETCD + +如果你只通过 RPM、Docker 或源代码安装了 Apache APISIX,而没有安装 ETCD,则需要这一步。 + +你可以通过 Docker 或者二进制等方式安装 ETCD。以下命令通过二进制方式安装 ETCD。 + +```shell +ETCD_VERSION='3.4.13' +$ wget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz +$ tar -xvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz && \ + cd etcd-v${ETCD_VERSION}-linux-amd64 && \ + sudo cp -a etcd etcdctl /usr/bin/ +$ nohup etcd & +``` + ## 步骤3:管理 Apache APISIX 服务 我们可以在 Apache APISIX 的目录下使用命令初始化依赖、启动服务和停止服务,也可以通过 `apisix help` 命令查看所有命令和对应的功能。 @@ -104,7 +121,7 @@ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-rep ```shell # initialize NGINX config file and etcd -apisix init +$ apisix init ``` ### 测试配置文件 @@ -113,7 +130,7 @@ apisix init ```shell # generate `nginx.conf` from `config.yaml` and test it -apisix test +$ apisix test ``` ### 启动 Apache APISIX @@ -122,7 +139,7 @@ apisix test ```shell # start Apache APISIX server -apisix start +$ apisix start ``` ### 停止运行 Apache APISIX @@ -133,14 +150,14 @@ apisix start ```shell # stop Apache APISIX server gracefully -apisix quit +$ apisix quit ``` 执行强制停机的命令如下所示: ```shell # stop Apache APISIX server immediately -apisix stop +$ apisix stop ``` ### 查看其他操作 @@ -149,7 +166,7 @@ apisix stop ```shell # more actions find by `help` -apisix help +$ apisix help ``` ## 步骤4:运行测试案例 @@ -159,13 +176,13 @@ apisix help 2. 然后通过 `cpanm` 来安装 test-nginx 的依赖: ```shell - sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) + $ sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) ``` 3. 运行 `git clone` 命令,将最新的源码克隆到本地,请使用我们 fork 出来的版本: ```shell - git clone https://github.com/iresty/test-nginx.git + $ git clone https://github.com/iresty/test-nginx.git ``` 4. 有两种方法运行测试: @@ -207,7 +224,7 @@ apisix help 使用以下命令运行指定的测试用例: ```shell -prove -Itest-nginx/lib -r t/plugin/openid-connect.t +$ prove -Itest-nginx/lib -r t/plugin/openid-connect.t ``` 关于测试用例的更多细节,参见 [测试框架](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md) @@ -231,7 +248,7 @@ apisix: 当我们需要访问 Admin API 时,就可以使用上面记录的 key 了,如下所示: ```shell -curl http://127.0.0.1:9080/apisix/admin/routes?api_key=abcdefghabcdefgh -i +$ curl http://127.0.0.1:9080/apisix/admin/routes?api_key=abcdefghabcdefgh -i ``` 返回结果中的状态码 200 说明访问成功,如下所示: @@ -247,7 +264,7 @@ Content-Type: text/plain 在这个时候,如果您输入的 key 与 `conf/config.yaml` 中 `apisix.admin_key` 的值不匹配,例如,我们已知正确的 key 是 `abcdefghabcdefgh`,但是我们选择输入一个错误的 key,例如 `wrong-key`,如下所示: ```shell -curl http://127.0.0.1:9080/apisix/admin/routes?api_key=wrong-key -i +$ curl http://127.0.0.1:9080/apisix/admin/routes?api_key=wrong-key -i ``` 返回结果中的状态码 `401` 说明访问失败,原因是输入的 `key` 有误,未通过认证,触发 `Unauthorized` 错误,如下所示: @@ -271,8 +288,8 @@ Content-Type: text/html 如果您使用的操作系统是 CentOS 7,且在步骤 2 中通过 RPM 包安装 Apache APISIX,配置文件已经自动安装到位,你可以直接运行以下命令: ```shell -systemctl start apisix -systemctl stop apisix +$ systemctl start apisix +$ systemctl stop apisix ``` 如果通过其他方法安装,可以参考 [配置文件模板](https://github.com/api7/apisix-build-tools/blob/master/usr/lib/systemd/system/apisix.service) 进行修改,并将其放置在 `/usr/lib/systemd/system/apisix.service` 路径下。 From 0d4f65a9ae06430388af609ca0dcad386225d616 Mon Sep 17 00:00:00 2001 From: Bisakh Date: Wed, 15 Dec 2021 09:15:59 +0530 Subject: [PATCH 191/260] feat(vault): vault lua module, integration with jwt-auth authentication plugin (#5745) --- apisix/core/vault.lua | 122 ++++++++++ apisix/plugins/jwt-auth.lua | 166 ++++++++++--- ci/centos7-ci.sh | 3 + ci/common.sh | 6 + ci/linux-ci-init-service.sh | 3 + ci/linux_openresty_common_runner.sh | 5 +- ci/pod/docker-compose.yml | 18 ++ conf/config-default.yaml | 13 + docs/en/latest/plugins/jwt-auth.md | 89 ++++++- t/certs/private.pem | 27 +++ t/certs/public.pem | 9 + t/plugin/jwt-auth-vault.t | 362 ++++++++++++++++++++++++++++ 12 files changed, 777 insertions(+), 46 deletions(-) create mode 100644 apisix/core/vault.lua create mode 100644 t/certs/private.pem create mode 100644 t/certs/public.pem create mode 100644 t/plugin/jwt-auth-vault.t diff --git a/apisix/core/vault.lua b/apisix/core/vault.lua new file mode 100644 index 000000000000..f98926d77491 --- /dev/null +++ b/apisix/core/vault.lua @@ -0,0 +1,122 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +local core = require("apisix.core") +local http = require("resty.http") +local json = require("cjson") + +local fetch_local_conf = require("apisix.core.config_local").local_conf +local norm_path = require("pl.path").normpath + +local _M = {} + +local function fetch_vault_conf() + local conf, err = fetch_local_conf() + if not conf then + return nil, "failed to fetch vault configuration from config yaml: " .. err + end + + if not conf.vault then + return nil, "accessing vault data requires configuration information" + end + return conf.vault +end + + +local function make_request_to_vault(method, key, skip_prefix, data) + local vault, err = fetch_vault_conf() + if not vault then + return nil, err + end + + local httpc = http.new() + -- config timeout or default to 5000 ms + httpc:set_timeout((vault.timeout or 5)*1000) + + local req_addr = vault.host + if not skip_prefix then + req_addr = req_addr .. norm_path("/v1/" + .. vault.prefix .. "/" .. key) + else + req_addr = req_addr .. norm_path("/v1/" .. key) + end + + local res, err = httpc:request_uri(req_addr, { + method = method, + headers = { + ["X-Vault-Token"] = vault.token + }, + body = core.json.encode(data or {}, true) + }) + if not res then + return nil, err + end + + return res.body +end + +-- key is the vault kv engine path, joined with config yaml vault prefix. +-- It takes an extra optional boolean param skip_prefix. If enabled, it simply doesn't use the +-- prefix defined inside config yaml under vault config for fetching data. +local function get(key, skip_prefix) + core.log.info("fetching data from vault for key: ", key) + + local res, err = make_request_to_vault("GET", key, skip_prefix) + if not res or err then + return nil, "failed to retrtive data from vault kv engine " .. err + end + + return json.decode(res) +end + +_M.get = get + +-- key is the vault kv engine path, data is json key vaule pair. +-- It takes an extra optional boolean param skip_prefix. If enabled, it simply doesn't use the +-- prefix defined inside config yaml under vault config for storing data. +local function set(key, data, skip_prefix) + core.log.info("stroing data into vault for key: ", key, + "and value: ", core.json.delay_encode(data, true)) + + local res, err = make_request_to_vault("POST", key, skip_prefix, data) + if not res or err then + return nil, "failed to store data into vault kv engine " .. err + end + + return true +end +_M.set = set + + +-- key is the vault kv engine path, joined with config yaml vault prefix. +-- It takes an extra optional boolean param skip_prefix. If enabled, it simply doesn't use the +-- prefix defined inside config yaml under vault config for deleting data. +local function delete(key, skip_prefix) + core.log.info("deleting data from vault for key: ", key) + + local res, err = make_request_to_vault("DELETE", key, skip_prefix) + + if not res or err then + return nil, "failed to delete data into vault kv engine " .. err + end + + return true +end + +_M.delete = delete + +return _M diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index cf3152a2a1a7..bf52fa094fae 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -19,6 +19,7 @@ local jwt = require("resty.jwt") local ck = require("resty.cookie") local consumer_mod = require("apisix.consumer") local resty_random = require("resty.random") +local vault = require("apisix.core.vault") local ngx_encode_base64 = ngx.encode_base64 local ngx_decode_base64 = ngx.decode_base64 @@ -54,6 +55,10 @@ local consumer_schema = { base64_secret = { type = "boolean", default = false + }, + vault = { + type = "object", + properties = {} } }, dependencies = { @@ -76,7 +81,20 @@ local consumer_schema = { }, }, required = {"public_key", "private_key"}, - } + }, + { + properties = { + vault = { + type = "object", + properties = {} + }, + algorithm = { + enum = {"RS256"}, + }, + }, + required = {"vault"}, + }, + } } }, @@ -119,29 +137,34 @@ function _M.check_schema(conf, schema_type) if schema_type == core.schema.TYPE_CONSUMER then ok, err = core.schema.check(consumer_schema, conf) else - ok, err = core.schema.check(schema, conf) + return core.schema.check(schema, conf) end if not ok then return false, err end - if schema_type == core.schema.TYPE_CONSUMER then - if conf.algorithm ~= "RS256" and not conf.secret then - conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) - elseif conf.base64_secret then - if ngx_decode_base64(conf.secret) == nil then - return false, "base64_secret required but the secret is not in base64 format" - end + if conf.vault then + core.log.info("skipping jwt-auth schema validation with vault") + return true + end + + if conf.algorithm ~= "RS256" and not conf.secret then + conf.secret = ngx_encode_base64(resty_random.bytes(32, true)) + elseif conf.base64_secret then + if ngx_decode_base64(conf.secret) == nil then + return false, "base64_secret required but the secret is not in base64 format" end + end - if conf.algorithm == "RS256" then - if not conf.public_key then - return false, "missing valid public key" - end - if not conf.private_key then - return false, "missing valid private key" - end + if conf.algorithm == "RS256" then + -- Possible options are a) both are in vault, b) both in schema + -- c) one in schema, another in vault. + if not conf.public_key then + return false, "missing valid public key" + end + if not conf.private_key then + return false, "missing valid private key" end end @@ -175,12 +198,62 @@ local function fetch_jwt_token(ctx) end -local function get_secret(conf) +local function get_vault_path(username) + return "consumer/".. username .. "/jwt-auth" +end + + +local function get_secret(conf, consumer_name) + local secret = conf.secret + if conf.vault then + local res, err = vault.get(get_vault_path(consumer_name)) + if not res or err then + return nil, err + end + + if not res.data or not res.data.secret then + return nil, "secret could not found in vault: " .. core.json.encode(res) + end + secret = res.data.secret + end + if conf.base64_secret then - return ngx_decode_base64(conf.secret) + return ngx_decode_base64(secret) end - return conf.secret + return secret +end + + +local function get_rsa_keypair(conf, consumer_name) + local public_key = conf.public_key + local private_key = conf.private_key + -- if keys are present in conf, no need to query vault (fallback) + if public_key and private_key then + return public_key, private_key + end + + local vout = {} + if conf.vault then + local res, err = vault.get(get_vault_path(consumer_name)) + if not res or err then + return nil, nil, err + end + + if not res.data then + return nil, nil, "keypairs could not found in vault: " .. core.json.encode(res) + end + vout = res.data + end + + if not public_key and not vout.public_key then + return nil, nil, "missing public key, not found in config/vault" + end + if not private_key and not vout.private_key then + return nil, nil, "missing private key, not found in config/vault" + end + + return public_key or vout.public_key, private_key or vout.private_key end @@ -197,16 +270,20 @@ local function get_real_payload(key, auth_conf, payload) end -local function sign_jwt_with_HS(key, auth_conf, payload) - local auth_secret = get_secret(auth_conf) +local function sign_jwt_with_HS(key, consumer, payload) + local auth_secret, err = get_secret(consumer.auth_conf, consumer.username) + if not auth_secret then + core.log.error("failed to sign jwt, err: ", err) + core.response.exit(503, "failed to sign jwt") + end local ok, jwt_token = pcall(jwt.sign, _M, auth_secret, { header = { typ = "JWT", - alg = auth_conf.algorithm + alg = consumer.auth_conf.algorithm }, - payload = get_real_payload(key, auth_conf, payload) + payload = get_real_payload(key, consumer.auth_conf, payload) } ) if not ok then @@ -217,18 +294,24 @@ local function sign_jwt_with_HS(key, auth_conf, payload) end -local function sign_jwt_with_RS256(key, auth_conf, payload) +local function sign_jwt_with_RS256(key, consumer, payload) + local public_key, private_key, err = get_rsa_keypair(consumer.auth_conf, consumer.username) + if not public_key then + core.log.error("failed to sign jwt, err: ", err) + core.response.exit(503, "failed to sign jwt") + end + local ok, jwt_token = pcall(jwt.sign, _M, - auth_conf.private_key, + private_key, { header = { typ = "JWT", - alg = auth_conf.algorithm, + alg = consumer.auth_conf.algorithm, x5c = { - auth_conf.public_key, + public_key, } }, - payload = get_real_payload(key, auth_conf, payload) + payload = get_real_payload(key, consumer.auth_conf, payload) } ) if not ok then @@ -238,13 +321,22 @@ local function sign_jwt_with_RS256(key, auth_conf, payload) return jwt_token end - -local function algorithm_handler(consumer) +-- introducing method_only flag (returns respective signing method) to save http API calls. +local function algorithm_handler(consumer, method_only) if not consumer.auth_conf.algorithm or consumer.auth_conf.algorithm == "HS256" or consumer.auth_conf.algorithm == "HS512" then - return sign_jwt_with_HS, get_secret(consumer.auth_conf) + if method_only then + return sign_jwt_with_HS + end + + return get_secret(consumer.auth_conf, consumer.username) elseif consumer.auth_conf.algorithm == "RS256" then - return sign_jwt_with_RS256, consumer.auth_conf.public_key + if method_only then + return sign_jwt_with_RS256 + end + + local public_key, _, err = get_rsa_keypair(consumer.auth_conf, consumer.username) + return public_key, err end end @@ -284,7 +376,11 @@ function _M.rewrite(conf, ctx) end core.log.info("consumer: ", core.json.delay_encode(consumer)) - local _, auth_secret = algorithm_handler(consumer) + local auth_secret, err = algorithm_handler(consumer) + if not auth_secret then + core.log.error("failed to retrive secrets, err: ", err) + return 503, {message = "failed to verify jwt"} + end jwt_obj = jwt:verify_jwt_obj(auth_secret, jwt_obj) core.log.info("jwt object: ", core.json.delay_encode(jwt_obj)) @@ -325,8 +421,8 @@ local function gen_token() core.log.info("consumer: ", core.json.delay_encode(consumer)) - local sign_handler, _ = algorithm_handler(consumer) - local jwt_token = sign_handler(key, consumer.auth_conf, payload) + local sign_handler = algorithm_handler(consumer, true) + local jwt_token = sign_handler(key, consumer, payload) if jwt_token then return core.response.exit(200, jwt_token) end diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index f5a17996dc6f..c620417647bf 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -40,6 +40,9 @@ install_dependencies() { cp ./etcd-v3.4.0-linux-amd64/etcdctl /usr/local/bin/ rm -rf etcd-v3.4.0-linux-amd64 + # install vault cli capabilities + install_vault_cli + # install test::nginx yum install -y cpanminus perl cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) diff --git a/ci/common.sh b/ci/common.sh index f27583b3b495..ec8b7e6ae6c5 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -39,4 +39,10 @@ install_grpcurl () { tar -xvf grpcurl_${GRPCURL_VERSION}_linux_x86_64.tar.gz -C /usr/local/bin } +install_vault_cli () { + VAULT_VERSION="1.9.0" + wget https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip + unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin +} + GRPC_SERVER_EXAMPLE_VER=20210819 diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 2939e827d2bf..765c1155a111 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -32,6 +32,9 @@ docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test3 -c DefaultCluster docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic -n rocketmq_namesrv:9876 -t test4 -c DefaultCluster +# prepare vault kv engine +docker exec -i vault sh -c "VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv" + # prepare OPA env curl -XPUT 'http://localhost:8181/v1/policies/example' \ --header 'Content-Type: text/plain' \ diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index 7916d1f95bdc..98a9be25576a 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -54,8 +54,11 @@ do_install() { CGO_ENABLED=0 go build cd ../../ - # installing grpcurl + # install grpcurl install_grpcurl + + # install vault cli capabilities + install_vault_cli } script() { diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index 1055372b660d..b632a59c7e7c 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -136,6 +136,23 @@ services: consul_net: + ## HashiCorp Vault + vault: + image: vault:1.9.0 + container_name: vault + restart: unless-stopped + ports: + - "8200:8200" + cap_add: + - IPC_LOCK + environment: + VAULT_DEV_ROOT_TOKEN_ID: root + VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200 + command: [ "vault", "server", "-dev" ] + networks: + vault_net: + + ## OpenLDAP openldap: image: bitnami/openldap:2.5.8 @@ -396,4 +413,5 @@ networks: nacos_net: skywalk_net: rocketmq_net: + vault_net: opa_net: diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 0a9dec0e74cd..b53b32ba6191 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -286,6 +286,19 @@ etcd: # the default value is true, e.g. the certificate will be verified strictly. #sni: # the SNI for etcd TLS requests. If missed, the host part of the URL will be used. +# HashiCorp Vault storage backend for sensitive data retrieval. The config shows an example of what APISIX expects if you +# wish to integrate Vault for secret (sensetive string, public private keys etc.) retrieval. APISIX communicates with Vault +# server HTTP APIs. By default, APISIX doesn't need this configuration. +# vault: +# host: "http://0.0.0.0:8200" # The host address where the vault server is running. +# timeout: 10 # request timeout 30 seconds +# token: root # Authentication token to access Vault HTTP APIs +# prefix: kv/apisix # APISIX supports vault kv engine v1, where sensitive data are being stored + # and retrieved through vault HTTP APIs. enabling a prefix allows you to better enforcement of + # policies, generate limited scoped tokens and tightly control the data that can be accessed + # from APISIX. + + #discovery: # service discovery center # dns: # servers: diff --git a/docs/en/latest/plugins/jwt-auth.md b/docs/en/latest/plugins/jwt-auth.md index 1ed30cedcc40..a5bad8bb84ac 100644 --- a/docs/en/latest/plugins/jwt-auth.md +++ b/docs/en/latest/plugins/jwt-auth.md @@ -23,14 +23,16 @@ title: jwt-auth ## Summary -- [**Name**](#name) -- [**Attributes**](#attributes) -- [**API**](#api) -- [**How To Enable**](#how-to-enable) -- [**Test Plugin**](#test-plugin) - - [get the token in `jwt-auth` plugin:](#get-the-token-in-jwt-auth-plugin) - - [try request with token](#try-request-with-token) -- [**Disable Plugin**](#disable-plugin) +- [Summary](#summary) +- [Name](#name) +- [Attributes](#attributes) +- [API](#api) +- [How To Enable](#how-to-enable) + - [Enable jwt-auth with Vault Compatibility](#enable-jwt-auth-with-vault-compatibility) +- [Test Plugin](#test-plugin) + - [Get the Token in `jwt-auth` Plugin:](#get-the-token-in-jwt-auth-plugin) + - [Try Request with Token](#try-request-with-token) +- [Disable Plugin](#disable-plugin) ## Name @@ -40,6 +42,8 @@ The `consumer` then adds its key to the query string parameter, request header, For more information on JWT, refer to [JWT](https://jwt.io/) for more information. +`jwt-auth` plugin can be integrated with HashiCorp Vault for storing and fetching secrets, RSA key pairs from its encrypted kv engine. See the [examples](#enable-jwt-auth-with-vault-compatibility) below to have an overview of how things work. + ## Attributes | Name | Type | Requirement | Default | Valid | Description | @@ -51,6 +55,9 @@ For more information on JWT, refer to [JWT](https://jwt.io/) for more informatio | algorithm | string | optional | "HS256" | ["HS256", "HS512", "RS256"] | encryption algorithm. | | exp | integer | optional | 86400 | [1,...] | token's expire time, in seconds | | base64_secret | boolean | optional | false | | whether secret is base64 encoded | +| vault | object | optional | | | whether vault to be used for secret (secret for HS256/HS512 or public_key and private_key for RS256) storage and retrieval. The plugin by default uses the vault path as `kv/apisix/consumer//jwt-auth` for secret retrieval. | + +**Note**: To enable vault integration, first visit the [config.yaml](https://github.com/apache/apisix/blob/master/conf/config.yaml) update it with your vault server configuration, host address and access token. You can take a look of what APISIX expects from the config.yaml at [config-default.yaml](https://github.com/apache/apisix/blob/master/conf/config-default.yaml) under the vault attributes. ## API @@ -110,6 +117,68 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 }' ``` +### Enable jwt-auth with Vault Compatibility + +Sometimes, it's quite natural in production to have a centralized key management solution like vault where you don't have to update the APISIX consumer each time some part of your organization changes the signing secret key (secret for HS256/HS512 or public_key and private_key for RS256) and/or for privacy concerns you don't want to use the key through APISIX admin APIs. APISIX got you covered here. The `jwt-auth` is capable of referencing keys from vault. + +**Note**: For early version of this integration support, the plugin expects the key name of secrets stored into the vault path is among [ `secret`, `public_key`, `private_key` ] to successfully use the key. In future releases, we are going to add the support of referencing custom named keys. + +To enable vault compatibility, just add the empty vault object inside the jwt-auth plugin. + +1. You have stored HS256 signing secret inside vault and you want to use it for jwt signing and verification. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "key-1", + "vault": {} + } + } +}' +``` + +Here the plugin looks up for key `secret` inside vault path (`/consumer/jack/jwt-auth`) for consumer username `jack` mentioned in the consumer config and uses it for subsequent signing and jwt verification. If the key is not found in the same path, the plugin logs error and fails to perform jwt authentication. + +2. RS256 rsa keypairs, both public and private keys are stored into vault. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "kowalski", + "plugins": { + "jwt-auth": { + "key": "rsa-keypair", + "algorithm": "RS256", + "vault": {} + } + } +}' +``` + +The plugin looks up for `public_key` and `private_key` keys inside vault kv path (`/consumer/kowalski/jwt-auth`) for username `kowalski` mentioned inside plugin vault configuration. If not found, authentication fails. + +3. public key in consumer configuration, while the private key is in vault. + +```shell +curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "username": "rico", + "plugins": { + "jwt-auth": { + "key": "user-key", + "algorithm": "RS256", + "public_key": "-----BEGIN PUBLIC KEY-----\n……\n-----END PUBLIC KEY-----" + "vault": {} + } + } +}' +``` + +This plugin uses rsa public key from consumer configuration and uses the private key directly fetched from vault. + You can use [APISIX Dashboard](https://github.com/apache/apisix-dashboard) to complete the above operations through the web console. 1. Add a Consumer through the web console: @@ -125,7 +194,7 @@ then add jwt-auth plugin in the Consumer page: ## Test Plugin -#### get the token in `jwt-auth` plugin: +#### Get the Token in `jwt-auth` Plugin: * without extension payload: @@ -155,7 +224,7 @@ Server: APISIX/2.4 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmFtZSI6InRlc3QiLCJ1aWQiOjEwMDAwLCJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTYxOTA3MzgzOX0.jI9-Rpz1gc3u8Y6lZy8I43RXyCu0nSHANCvfn0YZUCY ``` -#### try request with token +#### Try Request with Token * without token: diff --git a/t/certs/private.pem b/t/certs/private.pem new file mode 100644 index 000000000000..76f0875f9540 --- /dev/null +++ b/t/certs/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA79XYBopfnVMKxI533oU2VFQbEdSPtWRD+xSl73lHLVboGP1l +SIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7JBUXyl6pysBPfrqC8n/MOXKaD4e8U +5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4Clrd7shAyitB7use6DHcVCKuI4bFO +oFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHAM+47r1iv3lY3ex0P45PRd7U7rq8P +8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1+7njrVQoWvuOTSsc9TDMhZkmmSsU +3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbcBQIDAQABAoIBADHXy1FwqHZVr8Mx +qI/CN4xG/mkyN7uG3unrXKDsH3K4wPuQjeAIr/bu43EOqYl3eLI3sDrpKjsUSCqe +rE1QhE5oPwZuEe+t8aqlFQ5YwP9YS8hEm57qpg5hkBWTBWfxQWVwclilV13JT5W0 +NgpfQwJ3l2lmHFrlARHMOEom5WQrewKvLh2YXeJBFQc0shHcjC2Pt7cjR9oAUVi6 +M5h6I+eB5xd9jj2a2fXaFL1SKZXEBVT6agSQqdB0tSuVTUsTBzNnuTL5ngS1wdLa +lEdrw8klOYWrUihKJgYH7rnQrVEVNxGyO6fVs1S9CxMwu/nW2MPcbRBY0WKYCcAO +QFJ4j4ECgYEA+yaEEPp/SH1E+DJi3U35pGdlHqg8yP0R7sik2cvvPUk4VbPrYVDD +NQ8gt2H+06keycfRqJTPptS79db9LpKjG59yYP3aWj2YbGsH1H3XxA3sZiWHkNl0 +7i0ZE0GSCmEMbPe3C0Z3726tD9ZyVdaE5RdvRWdz1IloA+rYr3ypnH0CgYEA9Hdl +KY8qSthtgWsTuthpExcvfppS3Dijgd23+oZJY2JLKf8/yctuBv6rBgqDCwpnUmGR +tnkxPD/igaBnFtaMjDKNMwWwGHyarWkI7Zc+6HUdNcA/BkI3MCxwYQg2fr7HXY0h +FalewOHeJz2Tldaue9DrVIO49jfLtBh2DYZFvCkCgYBV7OmGPY3KqUEtgV+dw43D +l7Ra9shFI4A9J9xuv30MhL6HY9UGKHGA97oDw71BgT0NYBX1DWS1+VaNV46rnnO7 +gaPKV0+bTDOX9E5rftqRMwpMME7fWebNjhRkKCzk7CsqJN41N1jVTBJdtsrLX2d8 +UbY6EpjogFJb9L9J2ubUqQKBgQCk6oKJJbZfJV/CJaz6qBFCOqrkmlD5lQ/ghOUf +EUYi0GVqYHH0vNJtz5EqEx9R7GPFNGLrGRi4z1QLJF1HD9dioJuWZujjq/NgtnG6 +bgSXJqJc52Lc4wB99AyfuL2ihSrTFmjSRx7Puc9241hTha7Rgh+vNOkq2HsH9FR3 +TTRv+QKBgG5ph+SFenSE7MgYXm2NRfG1k8bp86hrt9C8vHJ7DSO2Rr833RtqEiDJ +nD4FbR0IObaBpS2VJdOn/jBYXCG0hFuj+Shxiyg/mZN0fwPVaRWDls7jzqqPsA+b +x3XKRAn57LY8UbsNpOIqZ8kjVLPZhgfYwfOI3yAeSMv4ZnRY/MWe +-----END RSA PRIVATE KEY----- diff --git a/t/certs/public.pem b/t/certs/public.pem new file mode 100644 index 000000000000..f122f85bb735 --- /dev/null +++ b/t/certs/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2 +VFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J +BUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C +lrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA +M+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1 ++7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc +BQIDAQAB +-----END PUBLIC KEY----- diff --git a/t/plugin/jwt-auth-vault.t b/t/plugin/jwt-auth-vault.t new file mode 100644 index 000000000000..c7d9e421c835 --- /dev/null +++ b/t/plugin/jwt-auth-vault.t @@ -0,0 +1,362 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $http_config = $block->http_config // <<_EOC_; + + server { + listen 8777; + + location /secure-endpoint { + content_by_lua_block { + ngx.say("successfully invoked secure endpoint") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + my $vault_config = $block->extra_yaml_config // <<_EOC_; +vault: + host: "http://0.0.0.0:8200" + timeout: 10 + prefix: kv/apisix + token: root +_EOC_ + + $block->set_value("extra_yaml_config", $vault_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + if (!$block->no_error_log && !$block->error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: schema check +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.jwt-auth") + local core = require("apisix.core") + for _, conf in ipairs({ + { + -- public and private key are not provided for RS256, returns error + key = "key-1", + algorithm = "RS256" + }, + { + -- public and private key are not provided but vault config is enabled. + key = "key-1", + algorithm = "RS256", + vault = {} + } + }) do + local ok, err = plugin.check_schema(conf, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + else + ngx.say("ok") + end + end + } + } +--- response_body +failed to validate dependent schema for "algorithm": value should match only one schema, but matches none +ok + + + +=== TEST 2: create a consumer with plugin and username +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "jwt-auth": { + "key": "key-hs256", + "algorithm": "HS256", + "vault":{} + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: enable jwt auth plugin using admin api +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "jwt-auth": {} + }, + "upstream": { + "nodes": { + "127.0.0.1:8777": 1 + }, + "type": "roundrobin" + }, + "uri": "/secure-endpoint" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: sign a jwt and access/verify /secure-endpoint, fails as no secret entry into vault +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=key-hs256', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + if code >= 300 then + ngx.status = code + end + ngx.print(res) + } + } +--- response_body +failed to sign jwt +--- error_code: 503 +--- error_log eval +qr/failed to sign jwt, err: secret could not found in vault/ +--- grep_error_log_out +failed to sign jwt, err: secret could not found in vault + + + +=== TEST 5: store HS256 secret into vault +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/consumer/jack/jwt-auth secret=$3nsitiv3-c8d3 +--- response_body +Success! Data written to: kv/apisix/consumer/jack/jwt-auth + + + +=== TEST 6: sign a HS256 jwt and access/verify /secure-endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=key-hs256', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + if code >= 300 then + ngx.status = code + end + ngx.print(res) + } + } +--- response_body +successfully invoked secure endpoint + + + +=== TEST 7: store rsa key pairs into vault from local filesystem +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/consumer/jim/jwt-auth public_key=@t/certs/public.pem private_key=@t/certs/private.pem +--- response_body +Success! Data written to: kv/apisix/consumer/jim/jwt-auth + + + +=== TEST 8: create consumer for RS256 algorithm with keypair fetched from vault +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jim", + "plugins": { + "jwt-auth": { + "key": "rsa", + "algorithm": "RS256", + "vault":{} + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: sign a jwt with with rsa keypair and access /secure-endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + if code >= 300 then + ngx.status = code + end + ngx.print(res) + } + } +--- response_body +successfully invoked secure endpoint + + + +=== TEST 10: store rsa private key into vault from local filesystem +--- exec +VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put kv/apisix/consumer/john/jwt-auth private_key=@t/certs/private.pem +--- response_body +Success! Data written to: kv/apisix/consumer/john/jwt-auth + + + +=== TEST 11: create consumer for RS256 algorithm with private key fetched from vault and public key in consumer schema +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "john", + "plugins": { + "jwt-auth": { + "key": "rsa1", + "algorithm": "RS256", + "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA79XYBopfnVMKxI533oU2\nVFQbEdSPtWRD+xSl73lHLVboGP1lSIZtnEj5AcTN2uDW6AYPiWL2iA3lEEsDTs7J\nBUXyl6pysBPfrqC8n/MOXKaD4e8U5GAHFiwHWg2WzHlfFSlFkLjzp0vPkDK+fQ4C\nlrd7shAyitB7use6DHcVCKuI4bFOoFbdI5sBGeyoD833g+ql9bRkH/vf8O+rPwHA\nM+47r1iv3lY3ex0P45PRd7U7rq8P8UIw6qOI1tiYuKlFJmjFdcwtYG0dctxWwgL1\n+7njrVQoWvuOTSsc9TDMhZkmmSsU3wXjaPxJpydck1C/w9ZLqsctKK5swYWhIcbc\nBQIDAQAB\n-----END PUBLIC KEY-----\n", + "vault":{} + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 12: sign a jwt with with rsa keypair and access /secure-endpoint +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err, sign = t('/apisix/plugin/jwt/sign?key=rsa1', + ngx.HTTP_GET + ) + + if code > 200 then + ngx.status = code + ngx.say(err) + return + end + + local code, _, res = t('/secure-endpoint?jwt=' .. sign, + ngx.HTTP_GET + ) + if code >= 300 then + ngx.status = code + end + ngx.print(res) + } + } +--- response_body +successfully invoked secure endpoint From c567cf32634cd848cd6314e6b7434f3ebd778ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 15 Dec 2021 13:58:00 +0800 Subject: [PATCH 192/260] chore: don't use rebase to resolve merge conflicts (#5818) --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e43448eb7c64..862a6ce20cda 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,7 +10,8 @@ Please follow the requirements: 2. Test is required for the feat/fix PR, unless you have a good reason 3. Doc is required for the feat PR 4. Use a new commit to resolve review instead of `push -f` -5. Use "request review" to notify the reviewer once you have resolved the review +5. If you need to resolve merge conflicts after the PR is reviewed, please merge master but do not rebase +6. Use "request review" to notify the reviewer once you have resolved the review --> * [ ] Did you explain what problem does this PR solve? Or what new features have been added? From 6ce20fcd238d207c44915f321d0e183443af0dd2 Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Wed, 15 Dec 2021 16:19:26 +0800 Subject: [PATCH 193/260] feat: supprot OPA plugin complex response (#5779) --- apisix/plugins/opa.lua | 29 +++++++++++- apisix/plugins/opa/helper.lua | 14 +++--- ci/linux-ci-init-service.sh | 11 ----- ci/pod/docker-compose.yml | 9 +++- ci/pod/opa/data.json | 25 ++++++++++ ci/pod/opa/example.rego | 45 ++++++++++++++++++ t/plugin/opa.t | 88 +++++++++++++++++++++++++++++++---- 7 files changed, 192 insertions(+), 29 deletions(-) create mode 100644 ci/pod/opa/data.json create mode 100644 ci/pod/opa/example.rego diff --git a/apisix/plugins/opa.lua b/apisix/plugins/opa.lua index f33c3d042b2a..cfe0b5810d47 100644 --- a/apisix/plugins/opa.lua +++ b/apisix/plugins/opa.lua @@ -18,6 +18,7 @@ local core = require("apisix.core") local http = require("resty.http") local helper = require("apisix.plugins.opa.helper") +local type = type local schema = { type = "object", @@ -89,13 +90,37 @@ function _M.access(conf, ctx) -- parse the results of the decision local data, err = core.json.decode(res.body) - if err then + if err or not data then core.log.error("invalid response body: ", res.body, " err: ", err) return 503 end if not data.result then - return 403 + core.log.error("invalid OPA decision format: ", res.body, + " err: `result` field does not exist") + return 503 + end + + local result = data.result + + if not result.allow then + if result.headers then + core.response.set_header(result.headers) + end + + local status_code = 403 + if result.status_code then + status_code = result.status_code + end + + local reason = nil + if result.reason then + reason = type(result.reason) == "table" + and core.json.encode(result.reason) + or result.reason + end + + return status_code, reason end end diff --git a/apisix/plugins/opa/helper.lua b/apisix/plugins/opa/helper.lua index 2a8cf94316b4..059ea0826203 100644 --- a/apisix/plugins/opa/helper.lua +++ b/apisix/plugins/opa/helper.lua @@ -34,13 +34,13 @@ end local function build_http_request(conf, ctx) return { - scheme = core.request.get_scheme(ctx), - method = core.request.get_method(ctx), - host = core.request.get_host(ctx), - port = core.request.get_port(ctx), - path = core.request.get_path(ctx), - header = core.request.headers(ctx), - query = core.request.get_uri_args(ctx), + scheme = core.request.get_scheme(ctx), + method = core.request.get_method(ctx), + host = core.request.get_host(ctx), + port = core.request.get_port(ctx), + path = core.request.get_path(ctx), + headers = core.request.headers(ctx), + query = core.request.get_uri_args(ctx), } end diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 765c1155a111..6a7ffbbfb9b2 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -34,14 +34,3 @@ docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic # prepare vault kv engine docker exec -i vault sh -c "VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv" - -# prepare OPA env -curl -XPUT 'http://localhost:8181/v1/policies/example' \ ---header 'Content-Type: text/plain' \ ---data-raw 'package example - -default allow = false - -allow { - input.request.header["test-header"] == "only-for-test" -}' diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index b632a59c7e7c..b5d8062c2ed2 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -402,7 +402,14 @@ services: restart: unless-stopped ports: - 8181:8181 - command: run -s + command: run -s /example.rego /data.json + volumes: + - type: bind + source: ./ci/pod/opa/example.rego + target: /example.rego + - type: bind + source: ./ci/pod/opa/data.json + target: /data.json networks: opa_net: diff --git a/ci/pod/opa/data.json b/ci/pod/opa/data.json new file mode 100644 index 000000000000..33565594f030 --- /dev/null +++ b/ci/pod/opa/data.json @@ -0,0 +1,25 @@ +{ + "users": { + "alice": { + "headers": { + "Location": "http://example.com/auth" + }, + "status_code": 302 + }, + "bob": { + "headers": { + "test": "abcd", + "abcd": "test" + } + }, + "carla": { + "reason": "Give you a string reason" + }, + "dylon": { + "reason": { + "code": 40001, + "desc": "Give you a object reason" + } + } + } +} diff --git a/ci/pod/opa/example.rego b/ci/pod/opa/example.rego new file mode 100644 index 000000000000..2eb912e08c52 --- /dev/null +++ b/ci/pod/opa/example.rego @@ -0,0 +1,45 @@ +# +# 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. +# +package example + +import input.request +import data.users + +default allow = false + +allow { + request.headers["test-header"] == "only-for-test" + request.method == "GET" + startswith(request.path, "/hello") + request.query["test"] != "abcd" + request.query["user"] +} + +reason = users[request.query["user"]].reason { + not allow + request.query["user"] +} + +headers = users[request.query["user"]].headers { + not allow + request.query["user"] +} + +status_code = users[request.query["user"]].status_code { + not allow + request.query["user"] +} diff --git a/t/plugin/opa.t b/t/plugin/opa.t index 3592f68c6a50..064661aea2a6 100644 --- a/t/plugin/opa.t +++ b/t/plugin/opa.t @@ -71,7 +71,7 @@ property "host" validation failed: wrong type: expected string, got number "plugins": { "opa": { "host": "http://127.0.0.1:8181", - "policy": "example/allow" + "policy": "example" } }, "upstream": { @@ -80,7 +80,7 @@ property "host" validation failed: wrong type: expected string, got number }, "type": "roundrobin" }, - "uri": "/hello" + "uris": ["/hello", "/test"] }]] ) @@ -95,19 +95,91 @@ passed -=== TEST 3: hit route (with wrong header request) +=== TEST 3: hit route (with correct request) --- request -GET /hello +GET /hello?test=1234&user=none +--- more_headers +test-header: only-for-test +--- response_body +hello world + + + +=== TEST 4: hit route (with wrong header request) +--- request +GET /hello?test=1234&user=none --- more_headers test-header: not-for-test --- error_code: 403 -=== TEST 4: hit route (with correct request) +=== TEST 5: hit route (with wrong query request) --- request -GET /hello +GET /hello?test=abcd&user=none --- more_headers test-header: only-for-test ---- response_body -hello world +--- error_code: 403 + + + +=== TEST 6: hit route (with wrong method request) +--- request +POST /hello?test=1234&user=none +--- more_headers +test-header: only-for-test +--- error_code: 403 + + + +=== TEST 7: hit route (with wrong path request) +--- request +GET /test?test=1234&user=none +--- more_headers +test-header: only-for-test +--- error_code: 403 + + + +=== TEST 8: hit route (response status code and header) +--- request +GET /test?test=abcd&user=alice +--- more_headers +test-header: only-for-test +--- error_code: 302 +--- response_headers +Location: http://example.com/auth + + + +=== TEST 9: hit route (response multiple header reason) +--- request +GET /test?test=abcd&user=bob +--- more_headers +test-header: only-for-test +--- error_code: 403 +--- response_headers +test: abcd +abcd: test + + + +=== TEST 10: hit route (response string reason) +--- request +GET /test?test=abcd&user=carla +--- more_headers +test-header: only-for-test +--- error_code: 403 +--- response +Give you a string reason + + + +=== TEST 11: hit route (response json reason) +--- request +GET /test?test=abcd&user=dylon +--- more_headers +test-header: only-for-test +--- error_code: 403 +--- response +{"code":40001,"desc":"Give you a object reason"} From 219847e86f9549456d919838c035da4b5a20a6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Wed, 15 Dec 2021 19:52:25 +0800 Subject: [PATCH 194/260] docs(google-cloud-logging): update document format and description (#5822) --- docs/en/latest/plugins/google-cloud-logging.md | 2 +- docs/zh/latest/plugins/google-cloud-logging.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/latest/plugins/google-cloud-logging.md b/docs/en/latest/plugins/google-cloud-logging.md index a3ec27db7f65..6fe84b4a5cf7 100644 --- a/docs/en/latest/plugins/google-cloud-logging.md +++ b/docs/en/latest/plugins/google-cloud-logging.md @@ -124,7 +124,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 ## Test Plugin -* Success +* Send request to route configured with the `google-cloud-logging` plugin ```shell $ curl -i http://127.0.0.1:9080/hello diff --git a/docs/zh/latest/plugins/google-cloud-logging.md b/docs/zh/latest/plugins/google-cloud-logging.md index 18503d1184f6..80bc7c5cdcca 100644 --- a/docs/zh/latest/plugins/google-cloud-logging.md +++ b/docs/zh/latest/plugins/google-cloud-logging.md @@ -60,7 +60,7 @@ title: google-cloud-logging ## 如何开启 -1. 下面例子展示了如何为指定路由开启 `google-cloud-logging` 插件。 +下面例子展示了如何为指定路由开启 `google-cloud-logging` 插件。 ### 完整配置 @@ -125,7 +125,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f13 ## 测试插件 -* 成功的情况 +* 向配置 `google-cloud-logging` 插件的路由发送请求 ```shell $ curl -i http://127.0.0.1:9080/hello From de65fc4f86dee0a7059308f759464ad886770bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 16 Dec 2021 14:08:41 +0800 Subject: [PATCH 195/260] fix(mqtt-proxy): client id can be empty (#5816) --- apisix/stream/plugins/mqtt-proxy.lua | 14 ++++++++++---- t/stream-plugin/mqtt-proxy.t | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apisix/stream/plugins/mqtt-proxy.lua b/apisix/stream/plugins/mqtt-proxy.lua index 13318e4114e1..fae0eb08f5f4 100644 --- a/apisix/stream/plugins/mqtt-proxy.lua +++ b/apisix/stream/plugins/mqtt-proxy.lua @@ -111,7 +111,13 @@ local function parse_mqtt(data) return res end - res.client_id = str_sub(data, parsed_pos + 1, parsed_pos + client_id_len) + if client_id_len == 0 then + -- A Server MAY allow a Client to supply a ClientID that has a length of zero bytes + res.client_id = "" + else + res.client_id = str_sub(data, parsed_pos + 1, parsed_pos + client_id_len) + end + parsed_pos = parsed_pos + client_id_len res.expect_len = parsed_pos @@ -120,10 +126,10 @@ end function _M.preread(conf, ctx) - core.log.warn("plugin rewrite phase, conf: ", core.json.encode(conf)) - -- core.log.warn(" ctx: ", core.json.encode(ctx, true)) local sock = ngx.req.socket() - local data, err = sock:peek(16) + -- the header format of MQTT CONNECT can be found in + -- https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901033 + local data, err = sock:peek(14) if not data then core.log.error("failed to read first 16 bytes: ", err) return 503 diff --git a/t/stream-plugin/mqtt-proxy.t b/t/stream-plugin/mqtt-proxy.t index 4f3796ba9379..ae46fa8cdcc9 100644 --- a/t/stream-plugin/mqtt-proxy.t +++ b/t/stream-plugin/mqtt-proxy.t @@ -316,5 +316,23 @@ passed "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- stream_response hello world +--- grep_error_log eval +qr/mqtt client id: \w+/ +--- grep_error_log_out +mqtt client id: foo +--- no_error_log +[error] + + + +=== TEST 13: hit route with empty client id +--- stream_enable +--- stream_request eval +"\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x00" +--- stream_response +hello world +--- grep_error_log eval +qr/mqtt client id: \w+/ +--- grep_error_log_out --- no_error_log [error] From a68a03cb969e0457ea83d495ea99f29df573f898 Mon Sep 17 00:00:00 2001 From: Nicolas Frankel Date: Thu, 16 Dec 2021 10:00:17 +0100 Subject: [PATCH 196/260] docs: Fix typo in documentation (#5830) --- docs/en/latest/plugins/api-breaker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/latest/plugins/api-breaker.md b/docs/en/latest/plugins/api-breaker.md index 8ec108b0f5d6..3e36adc38244 100644 --- a/docs/en/latest/plugins/api-breaker.md +++ b/docs/en/latest/plugins/api-breaker.md @@ -101,7 +101,7 @@ Server: APISIX/1.5 ## Disable Plugin -When you want to disable the `api-breader` plugin, it is very simple, you can delete the corresponding json configuration in the plugin configuration, no need to restart the service, it will take effect immediately: +When you want to disable the `api-breaker` plugin, it is very simple, you can delete the corresponding json configuration in the plugin configuration, no need to restart the service, it will take effect immediately: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' From b1cd36f1443bb5edc3aa07856a0f2405727a6df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 17 Dec 2021 08:59:07 +0800 Subject: [PATCH 197/260] docs: fix typo introduced in the refactor (#5828) --- docs/en/latest/how-to-build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 0991805c5ca9..35e6b576e1a3 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -91,7 +91,7 @@ Please refer to: [Installing Apache APISIX with Helm Chart](https://github.com/a # Switch to the apisix-${APISIX_VERSION} directory $ cd apisix-${APISIX_VERSION} # Create dependencies - $ make depsInstall Apache APISIX + $ make deps # Install apisix command $ make install ``` From fce2b44f5fe5c3358ec7dcdeb8fdb5530ee3e8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 17 Dec 2021 09:06:31 +0800 Subject: [PATCH 198/260] fix(sls-logger): log entry unable get millisecond timestamp (#5820) --- apisix/plugins/slslog/rfc5424.lua | 9 ++-- t/plugin/sls-logger.t | 76 +++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/apisix/plugins/slslog/rfc5424.lua b/apisix/plugins/slslog/rfc5424.lua index ff73d1983e9a..5d09a58a5165 100644 --- a/apisix/plugins/slslog/rfc5424.lua +++ b/apisix/plugins/slslog/rfc5424.lua @@ -78,16 +78,15 @@ local Severity = { DEBUG = LOG_DEBUG, } -local os_date = os.date -local ngx = ngx -local rfc5424_timestamp_format = "!%Y-%m-%dT%H:%M:%S.000Z" +local log_util = require("apisix.utils.log-util") + + local _M = { version = 0.1 } function _M.encode(facility, severity, hostname, appname, pid, project, logstore, access_key_id, access_key_secret, msg) local pri = (Facility[facility] * 8 + Severity[severity]) - ngx.update_time() - local t = os_date(rfc5424_timestamp_format, ngx.now()) + local t = log_util.get_rfc3339_zulu_timestamp() if not hostname then hostname = "-" end diff --git a/t/plugin/sls-logger.t b/t/plugin/sls-logger.t index 7e8b1eb8a373..296372c6235f 100644 --- a/t/plugin/sls-logger.t +++ b/t/plugin/sls-logger.t @@ -19,7 +19,21 @@ use t::APISIX 'no_plan'; repeat_each(1); no_long_string(); no_root_location(); -run_tests; + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + +}); + +run_tests(); __DATA__ @@ -37,12 +51,8 @@ __DATA__ ngx.say("done") } } ---- request -GET /t --- response_body done ---- no_error_log -[error] @@ -60,13 +70,9 @@ done ngx.say("done") } } ---- request -GET /t --- response_body property "access_key_secret" is required done ---- no_error_log -[error] @@ -84,13 +90,9 @@ done ngx.say("done") } } ---- request -GET /t --- response_body property "timeout" validation failed: wrong type: expected integer, got string done ---- no_error_log -[error] @@ -155,12 +157,8 @@ done ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -169,8 +167,6 @@ passed GET /hello --- response_body hello world ---- no_error_log -[error] --- wait: 1 @@ -188,9 +184,43 @@ hello world ngx.say(data) } } ---- request -GET /t --- response_body 123 ---- no_error_log -[error] + + + +=== TEST 7: sls log get milliseconds +--- config + location /t { + content_by_lua_block { + local function get_syslog_timestamp_millisecond(log_entry) + local first_idx = string.find(log_entry, " ") + 1 + local last_idx2 = string.find(log_entry, " ", first_idx) + local rfc3339_date = string.sub(log_entry, first_idx, last_idx2) + local rfc3339_len = string.len(rfc3339_date) + local rfc3339_millisecond = string.sub(rfc3339_date, rfc3339_len - 4, rfc3339_len - 2) + return tonumber(rfc3339_millisecond) + end + + math.randomseed(os.time()) + local rfc5424 = require("apisix.plugins.slslog.rfc5424") + local m = 0 + -- because the millisecond value obtained by `ngx.now` may be `0` + -- it is executed multiple times to ensure the accuracy of the test + for i = 1, 5 do + ngx.sleep(string.format("%0.3f", math.random())) + local log_entry = rfc5424.encode("SYSLOG", "INFO", "localhost", "apisix", + 123456, "apisix.apache.org", "apisix.apache.log", + "apisix.sls.logger", "BD274822-96AA-4DA6-90EC-15940FB24444", + "hello world") + m = get_syslog_timestamp_millisecond(log_entry) + m + end + + if m > 0 then + ngx.say("passed") + end + } + } +--- response_body +passed +--- timeout: 5 From 4855813e617f15594e8b2fd47514a424759c8d35 Mon Sep 17 00:00:00 2001 From: Carl Chen Date: Fri, 17 Dec 2021 11:39:16 +0800 Subject: [PATCH 199/260] docs(request-id): fix typo in request-id plugin documents. (#5832) --- docs/en/latest/plugins/request-id.md | 4 ++-- docs/zh/latest/plugins/request-id.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/latest/plugins/request-id.md b/docs/en/latest/plugins/request-id.md index 871c542b5938..7a29ade0290c 100644 --- a/docs/en/latest/plugins/request-id.md +++ b/docs/en/latest/plugins/request-id.md @@ -105,7 +105,7 @@ plugin_attr: - `snowflake_epoc` default start time is `2021-01-01T00:00:00Z`, and it can support `69 year` approximately to `2090-09-0715:47:35Z` according to the default configuration - `data_machine_bits` corresponds to the set of workIDs and datacEnteridd in the snowflake definition. The plug-in aslocates a unique ID to each process. Maximum number of supported processes is `pow(2, data_machine_bits)`. The default number of `12 bits` is up to `4096`. -- `sequence_bits` defaults to `10 bits` and each process generates up to `1024` ID per second +- `sequence_bits` defaults to `10 bits` and each process generates up to `1024` ID per millisecond. #### example @@ -115,7 +115,7 @@ plugin_attr: > - Start time 2014-10-20 T15:00:00.000z, accurate to milliseconds. It can last about 69 years > - supports up to `1024` processes -> - Up to `4096` ID per second per process +> - Up to `4096` ID per millisecond per process ```yaml plugin_attr: diff --git a/docs/zh/latest/plugins/request-id.md b/docs/zh/latest/plugins/request-id.md index 5cbc42c098ee..87ab8633af2c 100644 --- a/docs/zh/latest/plugins/request-id.md +++ b/docs/zh/latest/plugins/request-id.md @@ -104,7 +104,7 @@ plugin_attr: - snowflake_epoc 默认起始时间为 `2021-01-01T00:00:00Z`, 按默认配置可以支持 `69年` 大约可以使用到 `2090-09-07 15:47:35Z` - data_machine_bits 对应的是 snowflake 定义中的 WorkerID 和 DatacenterID 的集合,插件会为每一个进程分配一个唯一ID,最大支持进程数为 `pow(2, data_machine_bits)`。默认占 `12 bits` 最多支持 `4096` 个进程。 -- sequence_bits 默认占 `10 bits`, 每个进程每秒最多生成 `1024` 个ID +- sequence_bits 默认占 `10 bits`, 每个进程每毫秒最多生成 `1024` 个ID #### 配置示例 @@ -114,7 +114,7 @@ plugin_attr: > - 起始时间 2014-10-20T15:00:00.000Z, 精确到毫秒为单位。大约可以使用 `69年` > - 最多支持 `1024` 个进程 -> - 每个进程每秒最多产生 `4096` 个ID +> - 每个进程每毫秒最多产生 `4096` 个ID ```yaml plugin_attr: From c3ba11f271ba51bfe34e27dadc9ab7a19ff00e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Fri, 17 Dec 2021 11:56:44 +0800 Subject: [PATCH 200/260] docs(logging): supplement the supported logging service (#5843) --- README.md | 2 +- docs/zh/latest/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02febfbaccdd..3001650f2597 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - High performance: The single-core QPS reaches 18k with an average delay of fewer than 0.2 milliseconds. - [Fault Injection](docs/en/latest/plugins/fault-injection.md) - [REST Admin API](docs/en/latest/admin-api.md): Using the REST Admin API to control Apache APISIX, which only allows 127.0.0.1 access by default, you can modify the `allow_admin` field in `conf/config.yaml` to specify a list of IPs that are allowed to call the Admin API. Also, note that the Admin API uses key auth to verify the identity of the caller. **The `admin_key` field in `conf/config.yaml` needs to be modified before deployment to ensure security**. - - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md), [RocketMQ Logger](docs/en/latest/plugins/rocketmq-logger.md)) + - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [RocketMQ Logger](docs/en/latest/plugins/rocketmq-logger.md), [SkyWalking Logger](docs/en/latest/plugins/skywalking-logger.md), [Alibaba Cloud Logging(SLS)](docs/en/latest/plugins/sls-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md)) - [Datadog](docs/en/latest/plugins/datadog.md): push custom metrics to the DogStatsD server, comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server. - [Helm charts](https://github.com/apache/apisix-helm-chart) diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index d6ed28d0b52d..b2e43c2049a7 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -135,7 +135,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - 高性能:在单核上 QPS 可以达到 18k,同时延迟只有 0.2 毫秒。 - [故障注入](plugins/fault-injection.md) - [REST Admin API](admin-api.md): 使用 REST Admin API 来控制 Apache APISIX,默认只允许 127.0.0.1 访问,你可以修改 `conf/config.yaml` 中的 `allow_admin` 字段,指定允许调用 Admin API 的 IP 列表。同时需要注意的是,Admin API 使用 key auth 来校验调用者身份,**在部署前需要修改 `conf/config.yaml` 中的 `admin_key` 字段,来保证安全。** - - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md), [Google Cloud Logging](plugins/google-cloud-logging.md), [RocketMQ Logger](plugins/rocketmq-logger.md)) + - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md), [RocketMQ Logger](plugins/rocketmq-logger.md), [SkyWalking Logger](plugins/skywalking-logger.md), [Alibaba Cloud Logging(SLS)](plugins/sls-logger.md), [Google Cloud Logging](plugins/google-cloud-logging.md)) - [Helm charts](https://github.com/apache/apisix-helm-chart) - **高度可扩展** From 07630c730177a1748a61a3ed4f37fde17e695192 Mon Sep 17 00:00:00 2001 From: 123liubao <87936714+123liubao@users.noreply.github.com> Date: Fri, 17 Dec 2021 12:08:09 +0800 Subject: [PATCH 201/260] docs: add offline install to 'how-to-build.md' (#5829) --- docs/en/latest/how-to-build.md | 17 +++++++++++++++++ docs/zh/latest/how-to-build.md | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/en/latest/how-to-build.md b/docs/en/latest/how-to-build.md index 35e6b576e1a3..b5c82f3fe416 100644 --- a/docs/en/latest/how-to-build.md +++ b/docs/en/latest/how-to-build.md @@ -54,6 +54,23 @@ $ sudo yum --showduplicates list apisix $ sudo yum install apisix ``` +### Installation via RPM Offline Package(CentOS 7) + +Download APISIX offline RPM package to `./apisix` folder + +```shell +$ sudo mkdir -p apisix +$ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +$ sudo yum clean all && yum makecache +$ sudo yum install -y --downloadonly --downloaddir=./apisix apisix +``` + +Copy `./apisix` folder to the target host, run the following command to install Apache APISIX. + +```shell +$ sudo yum install ./apisix/*.rpm +``` + ### Installation via Docker Please refer to: [Installing Apache APISIX with Docker](https://hub.docker.com/r/apache/apisix). diff --git a/docs/zh/latest/how-to-build.md b/docs/zh/latest/how-to-build.md index 6775c89d41aa..59c211051dcd 100644 --- a/docs/zh/latest/how-to-build.md +++ b/docs/zh/latest/how-to-build.md @@ -54,6 +54,23 @@ $ sudo yum --showduplicates list apisix $ sudo yum install apisix ``` +### 通过 RPM 包离线安装(CentOS 7) + +下载 APISIX 离线 RPM 包到 `./apisix` 文件夹 + +```shell +$ sudo mkdir -p apisix +$ sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm +$ sudo yum clean all && yum makecache +$ sudo yum install -y --downloadonly --downloaddir=./apisix apisix +``` + +拷贝 `./apisix` 文件夹到目标主机,使用以下命令安装 Apache APISIX。 + +```shell +$ sudo yum install ./apisix/*.rpm +``` + ### 通过 Docker 安装 详情请参考:[使用 Docker 安装 Apache APISIX](https://hub.docker.com/r/apache/apisix)。 From 37d469e081d26ecd48ebede0518dc3afaef74e5b Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 17 Dec 2021 16:13:14 +0800 Subject: [PATCH 202/260] fix(install-dependencies): the execution of the script should not be interrupted (#5848) --- utils/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh index 6ffaba510ec9..b2bd4dfc8614 100755 --- a/utils/install-dependencies.sh +++ b/utils/install-dependencies.sh @@ -44,7 +44,7 @@ function install_dependencies_with_aur() { # Install dependencies on centos and fedora function install_dependencies_with_yum() { - sudo yum install yum-utils + sudo yum install -y yum-utils local common_dep="curl git gcc openresty-openssl111-devel unzip pcre pcre-devel openldap-devel" if [ "${1}" == "centos" ]; then From 5cde5ae0040f9fa7468dae12649276f07b0e9031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sat, 18 Dec 2021 08:29:57 +0800 Subject: [PATCH 203/260] feat: upgrade luarocks to 3.8.0 which converts git:// to https:// (#5825) The Luarocks 2.x is already broken. --- .github/workflows/fuzzing-ci.yaml | 1 - Makefile | 8 ++------ ci/common.sh | 1 - ci/linux_apisix_current_luarocks_runner.sh | 2 -- ci/linux_apisix_master_luarocks_runner.sh | 2 -- docs/en/latest/FAQ.md | 3 +-- docs/zh/latest/FAQ.md | 4 +--- utils/linux-install-luarocks.sh | 9 +++++---- 8 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/fuzzing-ci.yaml b/.github/workflows/fuzzing-ci.yaml index b23ec2d4133b..bb132da95772 100644 --- a/.github/workflows/fuzzing-ci.yaml +++ b/.github/workflows/fuzzing-ci.yaml @@ -50,7 +50,6 @@ jobs: sudo apt-get install -y git openresty curl openresty-openssl111-dev unzip make gcc libldap2-dev ./utils/linux-install-luarocks.sh - git config --global url.https://github.com/.insteadOf git://github.com/ make deps make init make run diff --git a/Makefile b/Makefile index adcd64dfed11..2b25defc57cd 100644 --- a/Makefile +++ b/Makefile @@ -155,12 +155,8 @@ deps: runtime $(ENV_LUAROCKS) config $(ENV_LUAROCKS_FLAG_LOCAL) variables.OPENSSL_INCDIR $(addprefix $(ENV_OPENSSL_PREFIX), /include); \ $(ENV_LUAROCKS) install rockspec/apisix-master-0.rockspec --tree=deps --only-deps --local $(ENV_LUAROCKS_SERVER_OPT); \ else \ - $(call func_echo_warn_status, "WARNING: You're not using LuaRocks 3.x; please add the following items to your LuaRocks config file:"); \ - echo "variables = {"; \ - echo " OPENSSL_LIBDIR=$(addprefix $(ENV_OPENSSL_PREFIX), /lib)"; \ - echo " OPENSSL_INCDIR=$(addprefix $(ENV_OPENSSL_PREFIX), /include)"; \ - echo "}"; \ - $(ENV_LUAROCKS) install rockspec/apisix-master-0.rockspec --tree=deps --only-deps --local $(ENV_LUAROCKS_SERVER_OPT); \ + $(call func_echo_warn_status, "WARNING: You're not using LuaRocks 3.x; please remove the luarocks and reinstall it via https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh"); \ + exit 1; \ fi diff --git a/ci/common.sh b/ci/common.sh index ec8b7e6ae6c5..612236e902d5 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -26,7 +26,6 @@ export_or_prefix() { create_lua_deps() { echo "Create lua deps" - git config --global url.https://github.com/.insteadOf git://github.com/ make deps # maybe reopen this feature later # luarocks install luacov-coveralls --tree=deps --local > build.log 2>&1 || (cat build.log && exit 1) diff --git a/ci/linux_apisix_current_luarocks_runner.sh b/ci/linux_apisix_current_luarocks_runner.sh index 2682a4fca3dc..a143d479d0a2 100755 --- a/ci/linux_apisix_current_luarocks_runner.sh +++ b/ci/linux_apisix_current_luarocks_runner.sh @@ -32,8 +32,6 @@ script() { sudo rm -rf /usr/local/apisix - git config --global url.https://github.com/.insteadOf git://github.com/ - # install APISIX with local version sudo luarocks install rockspec/apisix-master-0.rockspec --only-deps > build.log 2>&1 || (cat build.log && exit 1) sudo luarocks make rockspec/apisix-master-0.rockspec > build.log 2>&1 || (cat build.log && exit 1) diff --git a/ci/linux_apisix_master_luarocks_runner.sh b/ci/linux_apisix_master_luarocks_runner.sh index ed3e58c46b9b..a75fdf6c2488 100755 --- a/ci/linux_apisix_master_luarocks_runner.sh +++ b/ci/linux_apisix_master_luarocks_runner.sh @@ -36,8 +36,6 @@ script() { mkdir tmp && cd tmp cp -r ../utils ./ - git config --global url.https://github.com/.insteadOf git://github.com/ - # install APISIX by luarocks sudo luarocks install $APISIX_MAIN > build.log 2>&1 || (cat build.log && exit 1) cp ../bin/apisix /usr/local/bin/apisix diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md index 8038b9c14685..9f56094208e0 100644 --- a/docs/en/latest/FAQ.md +++ b/docs/en/latest/FAQ.md @@ -66,7 +66,6 @@ See more [etcd why](https://etcd.io/docs/latest/learning/why/#comparison-chart). There are two possibilities when encountering slow luarocks: 1. Server used for luarocks installation is blocked -2. There is a place between your network and github server to block the 'git' protocol For the first problem, you can use https_proxy or use the `--server` option to specify a luarocks server that you can access or access faster. Run the `luarocks config rocks_servers` command(this command is supported after luarocks 3.0) to see which server are available. @@ -78,7 +77,7 @@ We already provide a wrapper in the Makefile to simplify your job: make deps ENV_LUAROCKS_SERVER=https://luarocks.cn ``` -If using a proxy doesn't solve this problem, you can add `--verbose` option during installation to see exactly how slow it is. Excluding the first case, only the second that the `git` protocol is blocked. Then we can run `git config --global url."https://".insteadOf git://` to using the 'HTTPS' protocol instead of `git`. +If using a proxy doesn't solve this problem, you can add `--verbose` option during installation to see exactly how slow it is. ## How to support gray release via Apache APISIX? diff --git a/docs/zh/latest/FAQ.md b/docs/zh/latest/FAQ.md index 816093f3fc62..514885c1b613 100644 --- a/docs/zh/latest/FAQ.md +++ b/docs/zh/latest/FAQ.md @@ -65,7 +65,6 @@ APISIX 需要一个配置中心,上面提到的很多功能是传统关系型 遇到 luarocks 慢的问题,有以下两种可能: 1. luarocks 安装所使用的服务器不能访问 -2. 你所在的网络到 github 服务器之间有地方对 `git` 协议进行封锁 针对第一个问题,你可以使用 https_proxy 或者使用 `--server` 选项来指定一个你可以访问或者访问更快的 luarocks 服务。 运行 `luarocks config rocks_servers` 命令(这个命令在 luarocks 3.0 版本后开始支持) @@ -77,8 +76,7 @@ luarocks 服务。 运行 `luarocks config rocks_servers` 命令(这个命令 make deps ENV_LUAROCKS_SERVER=https://luarocks.cn ``` -如果使用代理仍然解决不了这个问题,那可以在安装的过程中添加 `--verbose` 选项来查看具体是慢在什么地方。排除前面的 -第一种情况,只可能是第二种,`git` 协议被封。这个时候可以执行 `git config --global url."https://".insteadOf git://` 命令使用 `https` 协议替代。 +如果使用代理仍然解决不了这个问题,那可以在安装的过程中添加 `--verbose` 选项来查看具体是慢在什么地方。 ## 如何通过 APISIX 支持灰度发布? diff --git a/utils/linux-install-luarocks.sh b/utils/linux-install-luarocks.sh index 6a6d6b4f0abc..f0e9a8edd637 100755 --- a/utils/linux-install-luarocks.sh +++ b/utils/linux-install-luarocks.sh @@ -22,9 +22,10 @@ if [ -z ${OPENRESTY_PREFIX} ]; then OPENRESTY_PREFIX="/usr/local/openresty" fi -wget https://github.com/luarocks/luarocks/archive/v3.4.0.tar.gz -tar -xf v3.4.0.tar.gz -cd luarocks-3.4.0 || exit +LUAROCKS_VER=3.8.0 +wget https://github.com/luarocks/luarocks/archive/v"$LUAROCKS_VER".tar.gz +tar -xf v"$LUAROCKS_VER".tar.gz +cd luarocks-"$LUAROCKS_VER" || exit OR_BIN="$OPENRESTY_PREFIX/bin/openresty" OR_VER=$($OR_BIN -v 2>&1 | awk -F '/' '{print $2}' | awk -F '.' '{print $1"."$2}') @@ -41,7 +42,7 @@ fi make build > build.log 2>&1 || (cat build.log && exit 1) sudo make install > build.log 2>&1 || (cat build.log && exit 1) cd .. || exit -rm -rf luarocks-3.4.0 +rm -rf luarocks-"$LUAROCKS_VER" mkdir ~/.luarocks || true From a11c2d199fa70bccc748b6bb8181ce98d31a6612 Mon Sep 17 00:00:00 2001 From: guoqqqi <72343596+guoqqqi@users.noreply.github.com> Date: Sun, 19 Dec 2021 19:20:09 +0800 Subject: [PATCH 204/260] docs: sync stream proxy Chinese version (#5793) --- docs/zh/latest/admin-api.md | 10 +++-- docs/zh/latest/stream-proxy.md | 68 +++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index fa7f0d65fecc..826a908f3302 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -985,11 +985,13 @@ $ curl "http://127.0.0.1:9080/apisix/admin/plugins/key-auth" -H 'X-API-KEY: | 名字 | 可选项| 类型 | 说明 | 示例 | | ---------------- | ------| -------- | ------| -----| -| remote_addr | 可选 | IP/CIDR | 客户端 IP 地址 | "127.0.0.1/32" 或 "127.0.0.1" | -| server_addr | 可选 | IP/CIDR | 服务端 IP 地址 | "127.0.0.1/32" 或 "127.0.0.1" | -| server_port | 可选 | 整数 | 服务端端口 | 9090 | -| sni | 可选 | Host | 服务器名称指示| "test.com" | | upstream | 可选 | Upstream | 启用的 Upstream 配置,详见 [Upstream](architecture-design/upstream.md) | | | upstream_id | 可选 | Upstream | 启用的 upstream id,详见 [Upstream](architecture-design/upstream.md) | | +| remote_addr | 可选 | IP/CIDR | 过滤选项:如果客户端 IP 匹配,则转发到上游 | "127.0.0.1/32" 或 "127.0.0.1" | +| server_addr | 可选 | IP/CIDR | 过滤选项:如果 APISIX 服务器 IP 与 server_addr 匹配,则转发到上游 | "127.0.0.1/32" 或 "127.0.0.1" | +| server_port | 可选 | 整数 | 过滤选项:如果 APISIX 服务器 port 与 server_port 匹配,则转发到上游 | 9090 | +| sni | 可选 | Host | 服务器名称指示| "test.com" | + +点击 [此处](./stream-proxy.md#more-route-match-options),了解更多有关过滤器如何工作的信息。 [Back to TOC](#目录) diff --git a/docs/zh/latest/stream-proxy.md b/docs/zh/latest/stream-proxy.md index a9f69ec0b8a7..8039b1cef87e 100644 --- a/docs/zh/latest/stream-proxy.md +++ b/docs/zh/latest/stream-proxy.md @@ -72,7 +72,11 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 ## 更多 route 匹配选项 -我们可以添加更多的选项来匹配 route。 +我们可以添加更多的选项来匹配 route。目前 Stream Route 配置支持 3 个字段进行过滤: + +- server_addr: 接受 Stream Route 连接的 APISIX 服务器的地址。 +- server_port: 接受 Stream Route 连接的 APISIX 服务器的端口。 +- remote_addr: 发出请求的客户端地址。 例如 @@ -92,6 +96,68 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 例子中 APISIX 会把服务器地址为 `127.0.0.1`, 端口为 `2000` 代理到上游地址 `127.0.0.1:1995`。 +让我们再举一个实际场景的例子: + +1. 将此配置放在 `config.yaml` 中 + + ```yaml + apisix: + stream_proxy: # TCP/UDP proxy + tcp: # TCP proxy address list + - 9100 # by default uses 0.0.0.0 + - "127.0.0.10:9101" + ``` + +2. 现在运行一个 mysql docker 容器并将端口 3306 暴露给主机 + + ```shell + $ docker run --name mysql -e MYSQL_ROOT_PASSWORD=toor -p 3306:3306 -d mysql + # check it using a mysql client that it works + $ mysql --host=127.0.0.1 --port=3306 -u root -p + Enter password: + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 25 + ... + mysql> + ``` + +3. 现在我们将创建一个带有服务器过滤的 stream 路由: + + ```shell + curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' + { + "server_addr": "127.0.0.10", + "server_port": 9101, + "upstream": { + "nodes": { + "127.0.0.1:3306": 1 + }, + "type": "roundrobin" + } + }' + ``` + + 每当 APISIX 服务器 `127.0.0.10` 和端口 `9101` 收到连接时,它只会将请求转发到 mysql 上游。让我们测试一下: + +4. 向 `9100` 发出请求(在 config.yaml 中启用 stream 代理端口),过滤器匹配失败。 + + ```shell + $ mysql --host=127.0.0.1 --port=9100 -u root -p + Enter password: + ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 2 + ``` + + 下面的请求匹配到了 stream 路由,所以它可以正常代理到 mysql。 + + ```shell + mysql --host=127.0.0.10 --port=9101 -u root -p + Enter password: + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 26 + ... + mysql> + ``` + 完整的匹配选项列表参见 [Admin API 的 Stream Route](./admin-api.md#stream-route)。 ## 接收 TLS over TCP From 4423509680a0c2103ce23b30a0da71735ef73e8f Mon Sep 17 00:00:00 2001 From: jackfu Date: Sun, 19 Dec 2021 19:51:32 +0800 Subject: [PATCH 205/260] fix(ua-restriction): refine plugin configuration check logic (#5728) Co-authored-by: jack.fu --- apisix/plugins/ua-restriction.lua | 31 ++++++++++++++++-- t/plugin/ua-restriction.t | 54 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/ua-restriction.lua b/apisix/plugins/ua-restriction.lua index 3683a15fba56..ec74e7592115 100644 --- a/apisix/plugins/ua-restriction.lua +++ b/apisix/plugins/ua-restriction.lua @@ -16,6 +16,7 @@ -- local ipairs = ipairs local core = require("apisix.core") +local re_compile = require("resty.core.regex").re_match_compile local stringx = require('pl.stringx') local type = type local str_strip = stringx.strip @@ -36,11 +37,19 @@ local schema = { }, allowlist = { type = "array", - minItems = 1 + minItems = 1, + items = { + type = "string", + minLength = 1, + } }, denylist = { type = "array", - minItems = 1 + minItems = 1, + items = { + type = "string", + minLength = 1, + } }, message = { type = "string", @@ -88,6 +97,24 @@ function _M.check_schema(conf) return false, err end + if conf.allowlist then + for _, re_rule in ipairs(conf.allowlist) do + ok, err = re_compile(re_rule, "j") + if not ok then + return false, err + end + end + end + + if conf.denylist then + for _, re_rule in ipairs(conf.denylist) do + ok, err = re_compile(re_rule, "j") + if not ok then + return false, err + end + end + end + return true end diff --git a/t/plugin/ua-restriction.t b/t/plugin/ua-restriction.t index 77c68748edea..82e665894655 100644 --- a/t/plugin/ua-restriction.t +++ b/t/plugin/ua-restriction.t @@ -739,3 +739,57 @@ hello world } --- response_body passed + + + +=== TEST 33: the element in allowlist is null +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.ua-restriction") + local conf = { + allowlist = { + "userdata: NULL", + null, + nil, + "" + }, + } + local ok, err = plugin.check_schema(conf) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +property "allowlist" validation failed: wrong type: expected array, got table +done + + + +=== TEST 34: the element in denylist is null +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.ua-restriction") + local conf = { + denylist = { + "userdata: NULL", + null, + nil, + "" + }, + } + local ok, err = plugin.check_schema(conf) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +property "denylist" validation failed: wrong type: expected array, got table +done From e08ec60883c5f1de733fc5c4ddd60e99b3e0f776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Mon, 20 Dec 2021 17:51:11 +0800 Subject: [PATCH 206/260] feat(splunk): support splunk hec logging plugin (#5819) --- README.md | 2 +- apisix/plugins/splunk-hec-logging.lua | 150 +++++++++++++ ci/pod/docker-compose.yml | 13 ++ conf/config-default.yaml | 1 + .../images/plugin/splunk-hec-admin-cn.png | Bin 0 -> 462846 bytes .../images/plugin/splunk-hec-admin-en.png | Bin 0 -> 444224 bytes docs/en/latest/config.json | 3 +- docs/en/latest/plugins/splunk-hec-logging.md | 143 +++++++++++++ docs/zh/latest/README.md | 2 +- docs/zh/latest/config.json | 3 +- docs/zh/latest/plugins/splunk-hec-logging.md | 143 +++++++++++++ t/admin/plugins.t | 1 + t/plugin/splunk-hec-logging.t | 198 ++++++++++++++++++ 13 files changed, 655 insertions(+), 4 deletions(-) create mode 100644 apisix/plugins/splunk-hec-logging.lua create mode 100644 docs/assets/images/plugin/splunk-hec-admin-cn.png create mode 100644 docs/assets/images/plugin/splunk-hec-admin-en.png create mode 100644 docs/en/latest/plugins/splunk-hec-logging.md create mode 100644 docs/zh/latest/plugins/splunk-hec-logging.md create mode 100644 t/plugin/splunk-hec-logging.t diff --git a/README.md b/README.md index 3001650f2597..23d2cb2dadaa 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - High performance: The single-core QPS reaches 18k with an average delay of fewer than 0.2 milliseconds. - [Fault Injection](docs/en/latest/plugins/fault-injection.md) - [REST Admin API](docs/en/latest/admin-api.md): Using the REST Admin API to control Apache APISIX, which only allows 127.0.0.1 access by default, you can modify the `allow_admin` field in `conf/config.yaml` to specify a list of IPs that are allowed to call the Admin API. Also, note that the Admin API uses key auth to verify the identity of the caller. **The `admin_key` field in `conf/config.yaml` needs to be modified before deployment to ensure security**. - - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [RocketMQ Logger](docs/en/latest/plugins/rocketmq-logger.md), [SkyWalking Logger](docs/en/latest/plugins/skywalking-logger.md), [Alibaba Cloud Logging(SLS)](docs/en/latest/plugins/sls-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md)) + - External Loggers: Export access logs to external log management tools. ([HTTP Logger](docs/en/latest/plugins/http-logger.md), [TCP Logger](docs/en/latest/plugins/tcp-logger.md), [Kafka Logger](docs/en/latest/plugins/kafka-logger.md), [UDP Logger](docs/en/latest/plugins/udp-logger.md), [RocketMQ Logger](docs/en/latest/plugins/rocketmq-logger.md), [SkyWalking Logger](docs/en/latest/plugins/skywalking-logger.md), [Alibaba Cloud Logging(SLS)](docs/en/latest/plugins/sls-logger.md), [Google Cloud Logging](docs/en/latest/plugins/google-cloud-logging.md), [Splunk HEC Logging](docs/en/latest/plugins/splunk-hec-logging.md)) - [Datadog](docs/en/latest/plugins/datadog.md): push custom metrics to the DogStatsD server, comes bundled with [Datadog agent](https://docs.datadoghq.com/agent/), over the UDP protocol. DogStatsD basically is an implementation of StatsD protocol which collects the custom metrics for Apache APISIX agent, aggregates it into a single data point and sends it to the configured Datadog server. - [Helm charts](https://github.com/apache/apisix-helm-chart) diff --git a/apisix/plugins/splunk-hec-logging.lua b/apisix/plugins/splunk-hec-logging.lua new file mode 100644 index 000000000000..531e9086a2dc --- /dev/null +++ b/apisix/plugins/splunk-hec-logging.lua @@ -0,0 +1,150 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +local core = require("apisix.core") +local ngx = ngx +local ngx_now = ngx.now +local http = require("resty.http") +local log_util = require("apisix.utils.log-util") +local bp_manager_mod = require("apisix.utils.batch-processor-manager") + + +local DEFAULT_SPLUNK_HEC_ENTRY_SOURCE = "apache-apisix-splunk-hec-logging" +local DEFAULT_SPLUNK_HEC_ENTRY_TYPE = "_json" + + +local plugin_name = "splunk-hec-logging" +local batch_processor_manager = bp_manager_mod.new(plugin_name) + + +local schema = { + type = "object", + properties = { + endpoint = { + type = "object", + properties = { + uri = core.schema.uri_def, + token = { + type = "string", + }, + channel = { + type = "string", + }, + timeout = { + type = "integer", + minimum = 1, + default = 10 + } + }, + required = { "uri", "token" } + }, + ssl_verify = { + type = "boolean", + default = true + }, + }, + required = { "endpoint" }, +} + + +local _M = { + version = 0.1, + priority = 409, + name = plugin_name, + schema = batch_processor_manager:wrap_schema(schema), +} + + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + + +local function get_logger_entry(conf) + local entry = log_util.get_full_log(ngx, conf) + return { + time = ngx_now(), + host = entry.server.hostname, + source = DEFAULT_SPLUNK_HEC_ENTRY_SOURCE, + sourcetype = DEFAULT_SPLUNK_HEC_ENTRY_TYPE, + event = { + request_url = entry.request.url, + request_method = entry.request.method, + request_headers = entry.request.headers, + request_query = entry.request.querystring, + request_size = entry.request.size, + response_headers = entry.response.headers, + response_status = entry.response.status, + response_size = entry.response.size, + latency = entry.latency, + upstream = entry.upstream, + } + } +end + + +local function send_to_splunk(conf, entries) + local request_headers = {} + request_headers["Content-Type"] = "application/json" + request_headers["Authorization"] = "Splunk " .. conf.endpoint.token + if conf.endpoint.channel then + request_headers["X-Splunk-Request-Channel"] = conf.endpoint.channel + end + + local http_new = http.new() + http_new:set_timeout(conf.endpoint.timeout * 1000) + local res, err = http_new:request_uri(conf.endpoint.uri, { + ssl_verify = conf.ssl_verify, + method = "POST", + body = core.json.encode(entries), + headers = request_headers, + }) + + if err then + return false, "failed to write log to splunk, " .. err + end + + if res.status ~= 200 then + local body + body, err = core.json.decode(res.body) + if err then + return false, "failed to send splunk, http status code: " .. res.status + else + return false, "failed to send splunk, " .. body.text + end + end + + return true +end + + +function _M.log(conf, ctx) + local entry = get_logger_entry(conf) + + if batch_processor_manager:add_entry(conf, entry) then + return + end + + local process = function(entries) + return send_to_splunk(conf, entries) + end + + batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, process) +end + + +return _M diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index b5d8062c2ed2..16497f237fe6 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -413,6 +413,19 @@ services: networks: opa_net: + # Splunk HEC Logging Service + splunk: + image: splunk/splunk:8.2.3 + restart: unless-stopped + ports: + - "18088:8088" + environment: + SPLUNK_PASSWORD: "ApacheAPISIX@666" + SPLUNK_START_ARGS: "--accept-license" + SPLUNK_HEC_TOKEN: "BD274822-96AA-4DA6-90EC-18940FB2414C" + SPLUNK_HEC_SSL: "False" + + networks: apisix_net: consul_net: diff --git a/conf/config-default.yaml b/conf/config-default.yaml index b53b32ba6191..ca923baf7e96 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -366,6 +366,7 @@ plugins: # plugin list (sorted by priority) - datadog # priority: 495 - echo # priority: 412 - http-logger # priority: 410 + - splunk-hec-logging # priority: 409 - skywalking-logger # priority: 408 - google-cloud-logging # priority: 407 - sls-logger # priority: 406 diff --git a/docs/assets/images/plugin/splunk-hec-admin-cn.png b/docs/assets/images/plugin/splunk-hec-admin-cn.png new file mode 100644 index 0000000000000000000000000000000000000000..e8997d1380c401c262c51e893778f57664c54f53 GIT binary patch literal 462846 zcmcG$WmH>H`!5*WwYa;xyIXN6?pEC0-JRk^i@R&F;w|p(6nA%+)Bnu<@ZP!ar&*J= zvVi2|XaDRWN?B1F2_6p~1Og#_mXS~ee&mBdFg38yz#~2PZgU_I3FxzgsG4W? z>AHo{7jR)zqPMH;2_>W8mpEjC5F zWd89?_t@k{`>!{!ofRU`e|;8BFat+~ob12eLjM1Iwy0SU^xxjM!b|i=(&ZIWU`XMo z0h0w5GH>Owd25Wn2c(p|V67&#^Y@PO=w6Rd)*ul`70l|f4NLP`x{0q;yZRe`n-c%! zhCJ#nrGqY1WApu?$Ed`H?@U0Lp5U4@bTN{ha;Yt!VwmX~J;%FfPFQBHgi zc_Y-**9QYZK|v|gU^ENhHKfR!hfV1J1P8?v^M`kKW_O&k)6hVY)#GGl9{rviWnZGA zpg=U(*pT_GI8?3c)aQmwMGj{L#IH8r5 z6%MOy-9&fbFA>+1dxq}K1~^H{Fsy`f!yYit8VhF7FYe0jq1<(T#`6&Ca2blBzqGTC ze=kW1c%7##`LVIFd3f8^meX|#*>L&Lq6Q)(V=ET4;y%mAN*jxc#@&AwIbc#MF#G^} zbnX>cYx8f`@mpG1F5LT6J+%{+1{@ul)d4f3s>)*hS9umj*J!OR%5q}|js^q1bEU^2 z?p|R(i_MJ>92`6;DJkAqd~;!yIZ0;geA}ua@3e}62BVmhjY?Bf)3?!F5Y)XXP_-tw zc6DBt1PWYR)63@ZzpZ)Et8;_{8GUqjX?5Gy_j2X=b73!VakV+`v6(C81s@(tW$+vc z^6>E*KQ&M;^;}GL5Wxtp44B})f}i|aWJa|=&#h~2j8Od{qNs>9ql%v7$d4r%mhf|(U(q>Eu5ZlM7=xNJpvjABI?aTdp5Vqkl3NthF92~runVO(phj;tR8{@}w z79K&)LP6M#)8kYS-bMODCxvk@4XkebS{cP_W9K@WMU~+VK4=*ev7wGCl zQ=^7$ad-FjdwF~=P(Nwm@joTbIa6w~MS_8WNj8;}m!HduDMM3t5Cv5b0rR=@Q8l3J z?EIVv1d^7fj>Y9yvDln0lvdW!iGC1%Tr&GOS*RI`CtP)HX}*|(QxbY=`eTai&udsJ zmNquB@|RU2+;ZeSHANrgtA$1HhRO@B<6&J=o8vpd%#0RX93nzoS%Nv^3do~V_;a!9 zC_ZA5qYc}MMW_)Y6aSdocEtWtjwV|G56p63QIJg##B$Kq|d%(u8*LOx* zzu4{id-jpvTT4P}Uc5&WI>S~|N9T$=M`+h?Ei#JLOWxRD2+Lo)(U1l8Klhb+$jm~- zxDEJ-YKw72vu5YSg5@$9y*0;Ayxh(}F(UKsq{S#+YIcFiy?6r$<$L6H!=fboiuh`r zGn&p+F!+fP4G%q~@3- z+b>$mf!xqt*3Norf<7N>AyajZk&1~L}^9GQ86*S3x)whH-67z4Cp@| zZv3X|jLF9`_~6`*A+vn#@m2NILPJ9#355MI4kmJ^SB0If)nvvWbq#vlk!DL2IhNzk zbC3!73r$NWq#ks2bx+RHWhj;$hI|k~7?{{cc0SkT_LfU^Nq!jmQw#zfK+1YH)nlk5ektfB+4so}H+gn1FLR=p_Wz_JEa3 z1S<)>5%h<5yJ;TeODCvVhcT^t_bm^OSKS+IbRW(-pvDud#mp5|RQ?u<24GvGXRBkx zoI&{7k}t-9u7UrHvGPMTY9z%I-M6cf`sxFv%nb5y~DQs8THjJ`<{o*@xkn)WG&l*t1mZTOd6+&)wV*fti9rKJG zgZd@~J5k*h|2iP;B7}rCAAh%5Jy0(JVVz2A zb;;fDm9BRiuu( zRhU-mbs?YIpY!`XhkPUDZDw5WiKJARJbmLntOLR`0uq zCy&DoPwC8CWoLUEuVTZ2nvUr}LrdHH@4&I6FFan?kg~FWz0Z!eQxZ`VfYItY+YstT zjge5UP?J%Tn_tFu+5I{3##yPkGxF7rlD}L&cyfO%LW@Go*qHQvrDyW6eI5@<^~*Q6 z7Hw_a{`K*$pTfxV-|FgMV+{KVbF#N!sguu&8ah74O6V$OD9c?58}1hKrb_gELc_v) z`}-j#?|g}RE*{TnCM6T%4VaMwB?|B>M@HnOt!2p?*w~FMEKm$TL_EEo;1}CW>LCgF zghR+-9>{pN&Fv677J%A7wcMuLyE2HU>O*UWyk8wDnox5}@0F-!V<>=tey7Qh50j_I z6fq)UIffnzGGmc(FzB^?obx-7v$J1*@eYs&Bqz}!YdB0ixF`6y&x(f`JB| zd3Sb9T;tQVmG7z=?g{zGSnA>(dj#6%y2Yg-p`Z{}b8@o5EU}3%7JE#toT#N|Ng;po zv$8H;O(b$(?e*1e?VTaP_`oc!EEzpF(bhG#O8fgW!6U#Yrl(Vnk+0QV0Z%QH0)rvf zqhopBZs4;g898W@e5OX{)*L`ofg6EB$nyBT+wzYyBqSsmH9tb3ba!QIk5jUF-&s)+ z#dfpV&foIBn~#${L(xLkcW$Pn(YxpP$XE_-g=8Xuo{x8Yofca=$2=(tu}1Upu;DP$ z(#h=bq2I52p^)pcs_yRhi86N(XDux)j;=G^>Az$4lxgUMLTFQDL4k3p_ID=Pnj|w-vO5cqInYUR^nQKtdcQ4>>Gk7`kj%CG)TiM3mr=ReTiSbA~tRBm2eTw-*Zk)+jRhMOV_wNUd04RYLhRLK_pAjade z4IPTh0%q67mMJ6OPqgox^WiYags=GA7qF!2gMa?uo}cGp#cAq{D}Pc~vbJX1$Pz%i z!D0~8kb*`gdX%&1DUG99|a*sBoEu5%GD4*F zXfOsxx#IJ`t*6Ty$+gAH)2dRtz3r#*QG-4z|yQD&+f%t94g( zqE6qhRM#bLEm31Al`Cy(#Qrz7JpD$|oJ7`^HR&fS4qY+4WGazT-sBTTNJM80V-`1_U$%bRC2@=G zMkLqSis`au3?p9uYRG0UaBr{73pwvvZFMj?E!+M*G7>7LqT*M$$}VW6&jt1|SDx(Q zhU`^O5OBt=&srk4NDT%$I6laYQPORaEY_c28^^^VG zJ3g^PK}HsJbd20^Z`K(Htw8*mm>?pTP5@w?t~dj_=nv(su0NlYD`?PG);e(`w28YC za4!NNxivKz^co0;6W&9<%3&kaYR z^iE7@v=I~M1&V*&Z3Xzug~8-elkmf)mtJR4H5cYO3Ep@Xr_}6F94Z=7{8$x(-cMKD z6K)4l)tU8FKa`43q1Z3?LqiI>iD|htt(zm`@W-m-U2u6(ARl2Lo-m$YymMgaFtqRv zn4}-(Lf0k;`7~Vb#ql0;%)f1$i+viI4}P#z!>Z!LW%uW75$u3oMEGgoQ+_8fOHDNE zVNe|h{BCh-4VG@ox{q($Q5dW)r?>5L9d+1Wr<2$i_w5oI&_L&b=Kn=G2NvIZul#5J zcaL^tXBHH2f5i;8&hX?jKHGw?X$afuH?cUaE>eEiUA9U*5D``AulWS{) zY%sVW5^5@9D7clp&nDC%a-rmU{e;E`rZIhg$^LFKh)u)^r0l2m;bbNwZYVUHPJv_u z5>>A{oa)0h@llq1mc>-6`r?S2O)qUvp>43wWI1{3pJ*bvU-!bk5W=)AxP(_HAW>+T zV3cANl4!@jW`JW@{E3S%XM+*u=LzTOqa6U#P}HDl(+~W2=~0Y<6^B=}>R!T(Pu5Vr zudj9cS(r2c8={iGz7_}YN&5XZBjczl#`?K8#$L&kco^gVA;^XR_D15-gL4Ax9b1aKJ>iaWkU_{o9&a6h`UWcUDG#o zbNp=VaK zv>6;;bUV8arlb2v=5gZH(^>@gDY6t;&86RyzE|)^a0rmWJD}jgi@bQO{k+5OGEIq& zhFkatWLyp=@bsEG-Ll*rU0kH~^lVd2%LK}W>Dfoke1O7y>v`^_*X4{0fR##HxqYko zpNv$BxzTNHTD~l%bh5H^`&K%-tG`xOh7RjZ>fM=Ykk~);Ztt<=Am~2oUY=I*=DtIoE121k0D?lvb&g=c{Ur zQ8EXtm2q&k;(%#0@lYT-baLV#;YlD&E`|<3j^*Bw&dki*2FD*v=JnV8a4^1KgSx!F z7Rzcmo2pRI%@+2++Vp=x0@Sm+iL1d*UOv&^QOO$+AiYjUqK?sh?t_CpQa}`o%i@q+ z|9OXsg9-xG)z|kfU44p+H!N2xD9hhE_lLWkN|ycFcSas?5u|G6&$Yd@?RWS#bCQ@6 zMsflf#uj)_f*K`}!R18;-Sb9OWyY3s24XRvgVk&IM*37DKlp@zJ=GxTGM2}$yMzJ? zcx}hdB_dk7JfoA4lsdls7@JiRr9j^vio@yTw?<^r?>3o!6v`07e1G@vd#moTs|DU* zd(7(cu3Sss>fl^Z3^cwzj$}BIRXOW_-}%&r_9=q5QK&Ca^ZDp*@vNMh)p~$s?D5jn zx79dV(vsTug!l~N-|5?LZSyhJXH7_S}Q%=Nbq;K zcP(DsAH7qKZQRcD>na*K+UjKkVe|De!sW!#5P^06L3K9^27!8d?nvk`aNlFg5$HW2 zzIM+4s?LdOySlNnOu@t^jDL?Wmp+K!|7U;+eD5h4<-n8Sr`Ls-Is*!g~0&T5HSU6D)h&A0XrpBa959Se7~oRWFQ5y zPK3BRS1tR4>DuAbHHS&|+O~(GGnL4a4pV-)KU@X^tZvnvFcx~sz~>FAt)t!amrbqd z7O4M6{RkNuIhxb($wiKUfWYO-FH!Xt(*Gn&Y?2gcy{)MoN8!+Guk^ST=$LG^e+ww2 z!ECTIk3#6`ofG@IYS`JBaPtCvdH~4_x}{|$tYKF5%T>>ZUqVvh5IE@h-fy`8^j(HR zO2P~S;C@L-DsP|Nh=>g88XNo1R=JVRJzQNSl$Bxa*V^n|S+B$+-iLmO2W0svX3B^E zbUPuZrG?AP3_r-U`D%2esj|~8`aVr|i$tJ&WtvOtk8ZIJ zxa5sS9z+cFR;GgE1FH@RVz=PnI`^Eb#mJPuX0dp@O**^$KKB>~K6jMc@57Dz`Jy3~DJdz3wgGOxPCTzD z-*{(TJ|iFzECS2+Fm@2fIz>-+q7&~EATI|qXwW~RvLY%Iq(3fqksGGFcJzO z-Q@Rb2;#&S=q50AkJgIXNzEFq@KpLVFpyY38OucsGZ@IGTRTYjV~Mfp!R6@02g$*{?GS(X~7M46_}{7&}t%|$77_~7qfqk+0!&!auf&@IzlDusH> zD`%kG?f_!Nqu;PYBz?zzyW{qRY~~6`$J)9=`R)Aa!0=Z-_h!v?=b)sdjDmuwtt|tS z0Z#(Eg(5j~?Y(fV%Gvep6}Ellyme8j_C2mtwFW8aJ5{h)9~g)z_n{Ts$Id$+!m>~b z5va1_t}W$ibY|vXdsjyhK!BDS+{V*yC<_v4EK#W6y5YUNoa%NepSLD0H)H~;4u9)g z@Sj=podDsJ+FNDaU~q`~-Nla|#;y>Bc15bRLLatR&d$#Db}z_DwiMt( zhx5sCbTh4*mNfxw&98hSiqj|ghWC_D4jt_M+)ufN_Aw#EtcF*~lq3^7g$<56!>yOZSrUo5J$m0D>osKZ6rGNrz{)pU<6nshpZr9` z({alt+Zd}U4go5UgJd-43KdW6(l(vq&yxtH+CA>2?E}Lz?J;BBT3>(=vJ-GJo_C{2 zhq@SPUvQHuL`zC+$3wmYTNyUJEt6^Eb{s0ctLOV6#-`NaPRMFIK=Zq*>e9NWyDuz2 z%mlnotRIyK%l?ulQyyAV(+-v8{obx|vQ!P1>9da4Jh`defXS79wdM6Tb)yRRNGCJL z2qh)8@lKn{yb$&OCg&nAvK&~e3qyeNsO;PPNT6DBTd*P~N7|)3giyhn{ND`F>_6UL z93E{x3=sW1Ac3j=`ueuLz5SI=34>feW?9o!uZ^R7+>Jv(gS7}kV*@H$-rl-S)ul5cT&65dBmS1l#{Jjcb(M? z3RJ-F?g#YrP(MJyz!7rh&}p`>WcLs)S0yE9M7lW$V1f)0dN<=DD?Em;+=BSESlJyW zRgE?D)v_ui0#ST8KF8={E0A!t)p)(NGHf^RXaj<|`xPF5YpxTu$|*Yuft+BBgN2S&dpg#8fTn99QD@*S`op)Lltvcs507Knx)XJDa})P{wQJnh{cWb9)$Pnla5|nvt$w~0 z)N^%rO^HQ#!-B`}&9m=!Md|DJW@Kj?(td+w^gQ<`lJoLG*@lCLmWyDiYHVd;Vdsf^ zdmHWd*jUpc4h+mJ81|QfLj3J6D|XV<@4{W`)f%RI(O-Eyw`x0tK0>MhA=O*`D=1%! zX&8{g#bjh~p6>$Gm%0Ns4RW@Ia@;ST3DAq^HyVn=sIxI5gn?(L&YRHvgUQloSF`Fg=*7oG<- zBXn2yoc($so)lA5M9$96zTD3bYV*FsgNKKY?Ix}=s7F*S$H;1V4MfUn-MZeZgoT6a zlViT^Eko-MbzfLmcr=+PtZ2_ErSIwd|)|0we8$ zFxoSLDzAn_l9t+DY7)bs!vClv7UU3P_SN`aEaIQ-Pl5^xHiQk;D)HMIZIn&&=74x| zS~&=M2+dP5?VyW95;TKZJVUx1#aw@162AR|O=U|S}G%vVw zA~U@lX0PdSGXhWquV-6$+mN+fM5vzwzjdk><;}eX_pS6p$5GFR%;~&57faR1L^rVw#WXai+%GD$4t42L;cwvA=oCED{+MZX zxxJ3@i_0VlRy7JzsJlCZJAkys;RW-bgB-g(;UY#BRA@RFI&ke?V}CAom(CBA zo831QAt^JnlntS?rK(H4Nds6Uq`?>VQgF~IA>rKwGgcG{)2fD-SWiR*won%Tq!A~2 zxdb@id5>kbf^-9(w3+J;wv?T6gB&`e;tBb12~5=ibH1`_fKL4GSF;-N0ee zg~{^^aiR9Cd7>;uhTsE&d=8sRExL}X82C^ep>e|`U~Z9fP|OuL-JZ_l3;Nu(9A9@=)q6Oz_HDIi zJf3#|+0dT_UtNDg&*9puWevQ48=O=U2El-8QM~b0+)l)*r;YAL_M|4um1&T|b;k#R z8oaj$)L(^c4opB{4N)WOmCo}G=gsqv^>PvZ1a24}OTx&G&i1w1>=_V1R; zIV3Sd*jObmOAX8jC7arJYHH&kJVSnKgFl3|Fdlr~x3DYC7a$NUQ^!5WQi5K*S6)C# zBO@6by30Nj%hJa9xI$abHUkGSAA-8yj~ z;iraKZGD4>fsuASo`I4}t7cj17I?gSk7$g&Z}(Ze0F+I!+6}+0Rb7468~ecGI$$ZTKOfzLLNM^F6l07w-)P0u+P~z#&V;{A`Fi^;d0T64!(L#A4usGK zO8L}O;x`kIK;&Krmodx}d|1G#DtdN= zcM6mNm*~ceW;Zn8;PX$QZo-zYQ01&~h!}%HGhn3N zb-*r8x~r9ZYh7x!R#(sh()CWXuN$M#?`P{&i+XyQli#jFQT9kES=`3J8cP;b|3q9e_+nOo~{jzY1zrL5c@kKc33W$_{VLr`muhV zKQ#VA_4HAx-AjDmS__o(~fjL}Svb|Lr!Q zrTbNl-!ajNS(QB%-9R}Gfu05sWSbv5GPTls48S-cM0EGc1boxJe-GC0x%d?okU-AE z1F7V85>KV@{a;kV;0@YxxdKU%KsXvA;7nT7fT**xbE^2{3n=ggdt!Gc>;@Fr*C)2U zM(p=me#y;{GKxTz?+-RzL86 z-h62D-ec?QBcD15OE+RF7W?r9Y$marx!a5_g$66?^61oSXtGdsJ2m|!L$HS5i7CSh zVSjRPH6)^7*rn^=^>>?6=Zu(!#}e>DgAxrB698*G!!38IO>6t<{CuBDE#R}P>|CWa zGzgpdXd^4*ln6Rs9U#3$(&ponv2l@gO9dX!F19t)@+uwQ^^COHNBlr z2#nqdBNKmyo^B2p_$CW&Pilp?0rubKMvolWQc=sB2Unpg{F4-jU&U;;yn!=25Sta1 zW(mQH07T?!0#GN;&w~~Xsy%Bv-0vD3`2_^fspE+;&m+Pm^WCyqK!HHsg+1^-V{}A5 zBBA!)B^!``qZYz}Se?MTy6h-CpWqnL66kwss{crv+=1fb#Vb_I^JfLQer!V~uC|K~ zq%79I)!b-UI!Qmz@AOdS;-29(YYf;u$KF!DrvZ@$sbiqD{|f@hX;uxaDG5(66Jg<5 zYk;aJZ@O6AW+1oyRaDTRyIFD4YxOw2V+9Iv(ihi7))pWkg%40RYYY;#qDWhk$^M3c zAO&|rvN}{9eLM;b%F5U_xeOecD-DWi)pvXL7Tx+R_Bil3YwY4fe8e@Eo>oLm$VCU_ z+#H?GkV4)nWar!JUVrwYegn?W&9y1fU%xW{%}h~sscls(*ZAjc8|fWPI2`6YV9neD zXTtD;*E8^gU=+53Op-u+C}Fycbe9~0>hI+OC6=adou;R&Br01rDSWjWW_P;6?@v%K z){D9sW$qv9Ldzy^CT1fyfD;|LlBYq44Pe#GRt5ldeYH(r-h!QsCC`K{h|TWr zF;mX~KJ;U{42?JdU(_ua6%PwJNd^({A(t{vfRG9RV*ra6j%#gNsjESKI65YV_`5ox z|I3+XsH8-`eh0F7vl5UAef4s=xGMWke0gY)$fqS_rCqp?_u#}W%0W-9T@5|O=U!DN z(`@x+@Z+_#go>Pz6BCG)CjK$=1foz&tq&+&yTb|@0J-RKU`}JM%__%1zEeawRvRARgFgm>ih0HI|*K&hV2Q2 zd|@v-`3Ve}=imL~2?c|Jb^B{@P#re^Z0R1Mp)qa}6To|YZS{q&bR(c&=lc9p?Q)s} zLua>Vf++5e|dSyQu)^16#$RGAgs7Qkr_riyCFSxXYtxstG$f$esMJ0e(OSn1 znSrVsmT>xAc;l|F?-vk8)nN{TGcC@(tJ5$thP5Un{JGS2Q}=fu5H^PY2Lu7yU0*1T z<2%{*bm=;w>N~`iAgetX#_LJnp=c0R^hlycnclQ?M>_SCHAQt>kGLC8=!0He$ zxmzr)v~#94mtYSLs%xy>Ami%x59&MHF}Z0VP6&qK3Uq&e^#ZyIauA~6Or=Db(oOfc zZ7O(F7h6xb#eQ%Qv`hmwd5;ylkbgk<#C`bmG>Qbs?!yq~_S@UuyP%tnLsnzqQOpgs z4#)5}<{nhl(_(n24Lm*qYNeA0i%0?Kc={|8)DChYx1TTMm$bw(woRftkEEKguVS z4(#2PXg0N9>m%xnTZ2b}MaAJvh~EG1w0sSII&S5zz!ZWA41P3O^0w+BLG+_Vy>{hg zW6Jb!I-f7t87)frg!{Y0t~0w>s#^5*USf_I4GiSyV7VNzH=LBCFA1uyJB3*78BX|@ z+$}kqb$vW%l)R9W4eD>Iju7TF+2ocDijwM81B$Xn0r#Uprnq1?00? zhN@70{U`Ri4MYP|r5~kYO#e%Fb>aI4Rp0M3AEYG54I!OjzNh8$r2)uaFo$+DG&J5- zP}*&FV*f4AVArp`?MKyK!sSDi-La8@A5}t{N6g1b!shdT<>n5 z1du3T2_J?9M90A2RiJ7AV_gH6q`xP*JAdEI(SAa-hY1XUdG9M@-EGF)1P706VJ~th zK6XdgA4lu6NPVlfp#JF=fYmG7`!D&t+HwXcPoiD!2-stl8jpyeIcrQRMVvTH`o+G5 zg_e<)f90<>9lvt<;V_HaB5m4_i;L^0Tug!-}PcfTlN5U@#3=A^4Off)W?zq zYC5|AW)7cQSZXuo`>ScIfls{_xV9JQES~Pie)urrwoQZ+kevG`bT$fl_m!OLpn)_1 zgW_b-^kW)V$4&lmn0o%emI@a18(?TR0f;1QaQojnnNjDU0SPxVvJU0oT1M!;g6 zU*6Y}B$*$cL=l0ihTe3Q&@wo?djHu;T10fxnOhjLP8p_@YAxtMLTfY4s3Bl!>taO zu$n981V5{&cslGy&?IFcqvi9?F5i zdN07FFAkFJWt<-i+y`NjX52Oog=p=uO=^aw+v-=7)_)UGuP|KT?};YC&qreTGb?9w zVu}Q+b8jtg=wiL^b{C6O&@Yf`!QHMHL%;;%;1}LCOf%CT5q}QIG*6&B5ktoC8lulk z9qrMN1o9V4FE%}wF0d>_BBQU&tEay*NUerv!syI3m^3Wes===9F-Z0#pxKnZ7P5?c z){P6*?RX@+@r_z`IOAfwkjzBpqgj!-d=on+scdLi-L2^a|E}`|E#vhjL9j2(b(l#% z6tt2`pT50&f?#7~)7I7oOYiG?s9Z0$ahCvp{4RiDbK|8yo)?*7;^gr;&}V!uFfB`5lDZ1RSV+x#XZw&3&uZ&vLDd6||M}qyOqS zFo?(H86%#6Hxw}QHr(<>t5tE4^1xQ2cEmSuM^WS z;Z4ftc4S#66emD|RY2mW-Fexv3WAWGe^aOrLxxGZdrD9SJ(3PsI#AeCmL{{E-CU+-g~Mdq_t|2Q>6>*lobmMscfX0UbUFIeF)_ z7@^>SE~OQ@njbV?Mi7=DmV=w8%bWY&#`TUP+kkopVt|L597aw@CM480BozIv=t*R0 z5+*A+R!2KepP}0zkdwV*j;1WA=#jTvDS|U=h!*yeGN*dyb@xDV{~83&+OyVIodXB! zyFx4~=u6_3C~CqSW|PvW2vVTH?4Rf$dz`maqsWh9dC|j5g1+~6|J_mk5!B8x>KLV- zQ;(&o_bYl}zJtXLwoI$}vk9eWx_Y$&vD(q#;DpjgUzgrUl561$BCQNdPq3~^fgWuB zG8B%X7u}s>__W`?j8GbK>+{6r*$$(v3%p0*mo^9%5*(;jc(fj7q|Y$PZgj&@5++Me z>amsYbux1Si?^oYqPYSS=)qd_1*nEbRtz*E;Vasw3+3g5cEwifF@#Y~<2XNu+Lk4#~OyE3(} z^cK}x)$2z#OlKvRd ziu$2~6VLYT&Z3J8FAlF%DNk&aS_d2iBr_Bk^jMUZh;da|14y{Iq%IzZ|MWrxkkVB! z<@)by1Azq|9-ad6;fcekdiE4L%o@Y4;M#7T(54Q!G2DO7{T}_op$2ra%+|>XjG>`0 zptY3fV49qso`&tp$Gyx9MvI6%NnHAn($a!8TKI)#68jfuR#Wj)<0n(cby*zvYjxh; zD+7p_4A4(mSy}l~k&@BTS?o_?ll{hY>u`Rg?%&{$r(@et-({5%n8~WtUxke@{##y` z4=5rRLVkDmIj;|p-TGY(QWG~c-$rAuCWRxW>&2-#>{oymJ|XYPmu>PP5vo}xlJ4g$ zOsubYEXcjM)Ng`n5AXZQB9&K$99=`i(OG+fc>gOKCz4^5K zD2qn@myD@&*WcRGQYqIp(1R?#zM?I-5rLVKm6K!j9rKLE=Oi$diVG~7fhU+oIyQY_ z{t*8N3URzU<}CCx%hYNwDU$e*ENZ&#^)#Md8yrj~wOOo{&NC3)KHGx=0kPFS?$Pvu zk6)t7ba$1}+G6{bQ25L?GPYlbR(U=L$>Q?bQL>H-mFf5_fuO-HA7Yy2BvX0gWFV5} z8m?9~RJ^Kb<{MDfDckpj73JvFWgDGX-kX?ZyI$snm}y~NA8PJ~^w#uk`%+$@$SdoF>|0nZ*IyV8v9BNE1P0B_l z3#$a|VRMz6Ai1=fU)V0fl~++CvlhM-p_6QEGeCn!Isp36fpGa0&~$NIu}@(b)nxLX zBc^`mTR$>yO@XB`(J&YAku!L#PS0|?zTMXX z=;V1=L4g=)nTZUL;+|?exXE~1rlw(IBQ7hk)B0e1eS?@PVBkRj)jQQ^@LMeS@qYB5 zuFlg>cl${LV=4cg4e1M&1|(mIc;7!k5VSJ90K7A2hhejnO_I~<|H=Y{0JfMeM;aRW zaKTBklD2CsZ8n0ro~Hzcz{oygF29eILx7GoU5z2P(8EtXGc^nW!LAD1S>WS=Rw~l= zNQcR#kxiy_9e^$&!pXbSFa`X@6E);&O+$X{dI=_JgIs%MWxDwZ@$OaLIp4FXVju*y zk6$bLgJv*6_#=GUbT~_3DBC*XVDcj(K{#9GYZiLDUaf+J8O8pXe*R+1b@S1c>GS;Z z$okn`Z&P`49nk#*fHghrkLOXPN7KoPj^hU>_4a}WUI78YO2qz=y}{q(<6)_(n2PT{ z1b}A_!|;A=)A%gp{D?Xfn*|GWk8MmoptaQM-yp>kzYG^R%@kJWlMtsfWlO#tQh`AD z_`0{A-NF;ueBnT+$lVHmN869tFM`JEN9^pNrKOc0>Mu1s{!5)k7yai!$oXJ_I@NvY za%nqQ`VO4-f9wHYFE(0E%b{O=_ah| z6M9sfL=!)W+1MLdyw@aN>#VwzuWR*Jxqz-QSKmvOJEpSJu2a-oUy$#rPg+=bldKEo z@6LSPxC)LA47zqabo{dm+BY*USr>+vf(o8ua}BBTn!cjw3e#hB&TlB%8qM<6Z2qEp z*(TbLqZ^B!8E1;_8<>?!7)3a>*&2!F{_4#YMY(?@jx*(#URL`nGx-I+cl@HA6ZKqM z_Fm3kz76LQJOPKFjm~FK7D+0d3&#K}${UfgYWbQF7aY3cXDe`+O+W|+TF(5be*XUM z({l+{`K7dGsosxXjSdr7)?25ih#!FNsCtd$X zT((Ti9TQU)zwg@(V|T~WrVFJm^0~8ma939uFUt(_IYOUIq9Enc83q8|*t#mI3_&{8NQwIg2fJIF0Y6;QYyNek4SfCr zbUNyJdc7D;tTBf!CeBwfRQgi^5PWyCN85L->sPx`c-PgCZr|Cz1$`#$agGQOZS#ga zAk_H-Xyu4I_W&@rrnXA#Y^l=msqWn!+iPCe3vxom$Oq4R`|OMlc@QeNs5f>wUD=XI zB2)h1N=MuK0z6%dzK309WlfifxC-Q`4wxud>V_ORsfdis#UHc%2kGAw;#5fBaF_mP zH&yJwe;GN4YiDm~pL|-o@Iw)xshlsl`b2d!+fMwsqKs(vc*%)Aj9_jMRn)9q1XAL`E-;pbg+lSEq#oT*F zHPvow!yu2MNU;F|0v134DbhP4f`(p02kE`{5>OCO5KwyWJrpUSLsSH$*AO5SrMG~T zgc|s6_u1#&d!P3?KfW=(AKw}{WRR7Wm37~bxO`9D8ha9UsFx2{YeHOh=of z$C{K4E?x|W3fvAmPM3PwdP1QVXk5X^KeDvKoc;4BuU;9#`i&W;F6qhG&~AY=Q&Ea= z77g=L_&#Qrx@k4G%I_R2Jcdw2g=2<>uFZJuMWqU!T#q^F`2O7fB$^Qsoi>!SOv`Fe z=Omdc3cVy@H^ZcRfL}1}9DsYh$~#mvqZVXQ z3Tx!>(TO!GT&%>~4XT#7#WQ*Btixx2M*1;SywoHIGwaNGx3iw$ovaW2))T`I*|<(lY*A z&t1P}S=C4oR@z77-in$AZEpVico-eqH)2JVO>LQY=fFJEcbV7MS9>~p;+IEhS?w0X zc`6qTI{4QU^UlT#Hkdl?@-%v0o6oa8t*$w5 zcNbb!PFahh2ZQNC@CNjb_cL(@Y)&^CMen+$JdTn^@9nx#-?)?e?%m6d@3p}6W};U= zWbt<6e0rDrqof8t9ljb6KVIJ8Nr;QvAR9{WA#cyW%H8rZR!dDSn>8}1Vz_H?a1ga( zW8~VAu)B+g#xS*dY;d7D@8#s(fAE0X#m2~@CsQ)@V6uo_I57BGa0qUCk7o^-P|p)1 zxwkKh4Pr9+=)#zM_pcZck~mY#Qs?_fx72lY-}3R%dygJ!cBfcVj`r8j7P7~6HBX)G zeXFsfWv=wX&I{W7zBUusWh7?)<*6POSMHhJuiyuz^T~;+Q^jYXW_cIybf|p55Q2EM z1z44+5=FVtst{5t4$-5D(04^ffuS2a)>*K5KZo0FdX7Rmt4AYM3r57XpYr6JD`dvY zjJ3^`43=;o+SSc5pMJLHd2I-np>I%KyfPrZeZh0xunKw(k};TtHng#K-p16PjU?~h zJzszSjh;2lvF-hcU@}orQNgo7P%zcRL`%&G`x-Sp?Lb&6wM}P!(QP5rtWEFmE-YRA z#SN)F9B!0AC9z5)b+Rxhy+0hjb?@xNx*w_pw~^dAe1I^%x-hmwK<|y^4H9{-kF=w# zZnItd>k{W(-iEIWwYChEZ>%$mmXN0|j|(Tw(s_vmAPS1A=p`MaJUQur4I2DE!8rB-PfNBurZdWbULY&By{-6tF*F0~!g%wBT|CB~TlI90oq|q6P10Yh z^=0O9*TPgCta-e`z+|N@a9GMtx~V+(Qn(_DtgSKFT6!xfuM8jA_HFT*4pBKO+XZ{% zg*4EcN&Lnup~JyBz?~?GRH8YLpfVYjy&vC@?W-V2dO)r)0Y{{8j?o42e`Sgt-$H60d_BCbaCj8qo&cG9GRcTy|*hzS(+-d@P zHl76$5e?PSs-?U$QJMSg!qU<*$6a2voO}1^1?@i2aHpG?pRef{a{-@lPU7e}XgE`D z@7$n1D1lRr`Be>|q{h;|(6F~QmElrv-h2fL+-}S@GBVOo_-6EFI7~8?Uaj@}T>JKF z*EVwW?fW=Zi1(a7a;s%woZjBUZ-2GGX@+w=F(Dyg9c^N75`-S3KiT%=W015e+bA5p zN;?+7s6Z_oJ|N-qdA90V`!|b{2cIVz6@`5n$w;~Ms*A@1nfrWSeEs@WY-M`y786s2 z9+hBLi2WlS2F`6$tLCEVDl>k)xTvX>WrM9D(G{Rj z%mr`Re=Su?6Pls6I6H=4?AjDzi_?=GlrV^mPsk|G{br_|;um^}{7!!IxZ`xG0}vzo z#p2;(yI)Cv?kGQEZ*|W%Yy5USU$R-fu#ueJthj`t%4~U7P$^yf>J%bP%ya8;_~@Ez zGmDWYN-&Bt+fxE7K*1=cB?e zg2{p@#H&h;K$UWSAE@nt_X%pf>=B(6JMXN?PEwuv{7N?hUw=s-g_|0Uuj0^TkBoMt zX0?7V8|K_WwZhDt9g#6Y5Po9f+ugNA$*gYuyvIVY_8d(TyF#87 zaiF99fxibnkP^?VfwJ=9q!WreojNWdObAu1+hppVmahjo2J;CoJ~|3dtrXRw<9+pU zV~0o4ZuW|8r=u{c@qG1(xe@av3vqx#|_`wAM7t1Y_CV5O;95y ziNy^KFRg0yWWzEGfCW)3)xC@KcGg}3JZpRtlWMw0$}DV!!pLQZ)o&XsOdqgWR;C23 zbBVG;nS2ZroCkS2jpzraTJ>zt7JUKRUQh@vucDV&RDy{HWpcOJ#0MVlh!+$TtmRS7 zq2KNC0PFj4s`fpqx3VQ?<(%+A$a$(bJNv1Tgxd#hMjdTQP?%=CqZ>-mHQOM2kD_^| zVo-|k-8^dyb|SD4owmWak)?WWfL*|#n+PMaU9oml&Dwrz9YJtdIHet$+c-nqGqJut zW2(nUuSwD|G|}s`Qce|+D>*sfd{msP_B9JxsRemh5Y>Re#Nd)sp3))PBy zYD7dNF)?w)K~9g*)7V|N8pNyFJ1I(5atOQfRUt>}8}t<;BR4FqE!Ur}8y8?J52x!L z_r#(ad;-ev2`u!v<1n$?F$Xa&hP4w!`sfNHkMp^$k@FdEq=x3GA@+3T$Nd|K$ z`|)ux6&JOS#y)tHrlsiS=`EL^+uKF;CP8vgdz7>csN!IP&1X?$E(~Nq#1{5PjRq;& zhcG||+$>Vj@$!=M>F(;%@t$_e1Erp$KV4(Ozkg?u{5|9#c~be%$nnYWu-`&Ll0e|D zK*A)vP2lo2eVOrrSAsg$=`TyY0)=C@N<2vCj?Vt}unJUo;{|Jcv188JwjaTak%uxz zdJSN>{HsoAeSaB+ps=0M)nOC2h9BzI-G$#+K{LG+)4zD4cxxzUKEOth#jvDzL@V9M zm3V`8jWezu;@f?sa4aTwJ>(npwGGZVlv-AEvkj&hGKHL$Bz8`w=!l8XKAjln>toA! z0&j*&A|RsTc2sb|L{&~6iX(q!#{gJ3-*~f$Qm8it6rCp4Lu2Sld&j!5b`8PkCKkOF z11oobzpswonCjhhgkr9m)nY@QTROU)YoO*G)i-~XHq$I_mX>xxKkDay=fRh@=SR5b z%@jCV?AAT%Zg0Fk!m2CjwED;(S@{>?*%GxG3g1X^H99i_R3_Yqa6EdXW^4>%O7|D( zJSFqb&N0qyrS<0sVJ%n*!tH2Ro_X74m}yJuUz&&L^}`Om0uBY9PpZ$`Iru~vRmuZx z(-hoiNQ9bcvFI~uTRTY+g-?WM^Z}=}h%ThxXl-wi4&(V3;({&b3FS=W;XMCLKrc2n zAK2!Prpb%I#c}I!)^D{+INsmlgwP9}-l1R!jK;KCT7o*ELVa1ahas)(>||6iA5nIr zQNlB&49-p3Ud={qHIwC~y}!O@ZzIpM3eT75Q^;jWNGZm@$b_&$Fm(>&5lcv+)rmnV zTt1WUDFnPaTG^y;-WjdqH$@y87Ro8Y@*R5^-V2KIps}0~b3Q-t#lIBXZH@PIFI$ zsp<7~OnvU%r|V|~ic${#y)%E0_9)>Egw8p9e*Qi}thTM_E^>Z(wZ>{T4?XLnLlY^d zW8$6>cw|}Cr4TG4mhda9=iqH@>iu@3MHlJS{{*(Zih`>~3Hkdrj!@>?wI6%x zS6ftpMiIr3xFWleMfTmDz0nMBZSCg)1cZ*=rZZm(s#Lq<1QJNRO~(}l^kce8QE+g` z(vqfsXtwydUws(4C(kuaLh2J99#sP&=aF1F>9vCd&PA7a`;o8$zyR z6p>n1@~Ppist#OyudVn}a0f4#pjor6`vmk=+}etXO@AuF?&hd|(ubxAE`enaG2GI( zDU$1_R0SG5-V0~T?@~~Q?bSA1$YCGugQ=wAk z>iBq`zjyhGd#I4pGz%~}#;txmR*2HFKJKE?dujdSKX+mfRMMGn_ppp|MS0^*JMp~p zH9rUZsM-`=|V%A1R zb*o3XN9WB>VD%*^A(PQkWoete%zP{_sY>((G2qJfAHXu^wNaU$NJ{{sJ>Zh z+IPe|ZR-b5y%T_Mn50qs4HimfvjG1R{f3XyNH#G^tTrg+ByGK-^59W*Q1XR&nC0!T zdF}}4#`=o<+n5VCPr+?9;aw9If%Al)7Ww44Hf3~nMr|4Hy*Ygk(zLdW8QU02`c46u zo}TD;v?hVAo%l-Hz_q4PsG(uYREM@!SYL1N=zNE?px=PH`ta`FfsUzZTX0CQpzZE0 zbX_YuMNrJ2&bt3R(o?+x5jr!KrL7~Xo=XKPNAP@}3X6uPGjTumXa zRZ?fGB=zeSv054$WeL4OqjV&sjnD2;7HQtQm*Yf8_tg3@QEge1dAu^`iE-p#T-bnT zXW!K+2#cLJlRVi(?gs@mtI|YvEFjNMeY4hf*K`{D>yH;Q#OpUw;YIf2Au+|~MJj?9 z99dae{f-J~Yxk2n5CiJ5J&bs<`h_3Lt-1Gg*ImSL1L72wnL26f8$p!93j@uDD_@$o z+qBJ_QSGT+KvNM;8jDyVpVZ68@I!t%`?Vyo9OD$N=UbSj^>us8DqW8Q$pd@5EY%&{ z;2Vwy14uPs#<#8J+FF~oXU{cd0^0zaLeQ^IH%jFES!fuv^&mbW!FIrZ`;iFC^E@8L zhjFIGs@rXT3*`s6lb$8Nm2zRv->(Q)3m8?3oLbJ-<@*o*JG?4}4f)(;Q}U7wL`IW& z|M{qu=!Wdsopmya?OB8(qnMZ0`WZ|g>GTgJTIV=pm^Zhww_FPRU?Ami&6R=J{Bnc3 zM-Lvf%FhAS*KlTxRi4z`K6MFbCCr8kR$Q%jn$gIcJI2`?5etNmmVibQScT~l>GM^q{UyUs56L{LuGo%5BXZjEHB^yU<6rL7mTg5w&WP zs9oyGd)wz`BX%C)67U4UG|11)QdvP2L1HFTr^{9*D99`-$Xk9kNZ6o^Gv0$ zOm@#@LBX8jm*Z_mgX%YahAjjge7@OXsae>_6tpvG2+;sYAMglfSF4o5ez_ti>RctEVcnbfYWRReP;;=e zC$boc!Xml|TgZd+b%|yB%C<7JF|2VMD}BBn5_q;o8iN6ZsMV_0HgkoMUY^$MBon_J zd4H>A8RgsF@4H~o$LHFhh?oP8DjmpZ<`64H^z>puGfNGPc@@!&OcFO$!p1h(fMN?D zWOjFO!==*aT2{+nFAZwPLMslHM|CRHo^}1@iJg*dY!ub6Iaal_)Gmu_bQqb?KQL-= zX#=QGB?uxo#KhzcfV)-EK5lr6mUivU$vGQDVRZuH>gvkD$vL`?3L4kFZ9HS06<7ws zv<4Yzl4vDRuVwV63TKZNXyQvSP2R68%7R~hlqi1d#*N(9Um|b@-Q8OWcUoFuVXpQF<@m^aK zv&C7&I7L-B**0kFS7Us5z`s7zoW-5Sr;WWS)^suaj!F^eJyr%D+gIufXIstAg6_-h z5wG&Oo5mnep_Wn|-9{OgTjI7`RL7WF*tMlB%B>-0r@A8h!@TZM~UHyk++5Xt>c zCy7ZIPt<-10vjg(E;3TbLaU>x_d_2!b~Z!H`n>6zCB+l_Dl6@g&d8(V;^pu{Z(go`?Hvseo4E^~M-JNU z8?>xiBb?`Cf=)Z6?|r+Qlv?$k+yR3 z#=Kna(ok%ym+i#6NRN#u5Vyc9cXb6F`lmeomXB!n_Xa)ZEGo z2wGIEFN<%DwZurP$(ZJ}9UmL?pP02J66(I89{Sry2R3fR zG4>D6Z#&aj#TjJH13dPeRk@kA@P~20W)G20)@V;S8osyhBbUy!OlC0|*^^KssiTLP zk^Zs5N^8>*VxPd<2H@cr!0#eR2&RX*-P}t!MZ%`eF;$SttrITgBmb)jyrL>^7)dO4 zN}6h(`nopq2`}4W(pjhrXc^-cbXGvPCwgq14-Sy7FD_yI|l*$2MTg>a>pJO{vxjU_L{|Q=Irkoe+{Khr3wbw0VTm@ z$ZHZAc%~jJV6m#k>LJmO>T;^L$CXOs#?vati)rG$?rPW1dF284Cl}z_85YS3 zIL+y^stG)YS?1yWE$~XAVRA2HM@B~^pjB}kr88~EJ&F|cd)4fthT7YSkB#%UktfjD z_@tx-8ZWrk$E?aOW>~TMp8V(!9vj2EXEsaAOF#{GmiPbiSPwcs=`1g=`>j`Cof)Vh zl%`yCAl;6tvp?V_Y#ThQu%Z8l(831=9KN&e;`WkeoX{g#NNro&{xsoq@(;h0b&FW& z5DC}d6B9$7m%JPgX~jWsX2-w)KlE}1Bt2B7B>PlKeEt}rL@+E}z_cavXhqsWOL6I7 z(>sihk8|sIM5?0PZ-0}Wnc3NKx)zdF?~pJv>+!Yl$GgiJfqo{Tf05d^cPdX+dp2)G zOH)fn)?=oDW#Ztpr!$ejZ4Mg&1)ynb+pah*%-U|I@v1B<08puTICO@;nt@oTIH=3~ z`=({0No{4i3cGY;_INY3w)IXu$D;w&x>r02%kdn}CT|q@)9`cdU-DY1hXzz*YheT2 z*fTF@l)UV2zURkYm%gnf5T!h&W$)q z&$SU{24Sb6ULHvk^Ai->urjmNuQ{`5Yp&9FPYP9MC!FeZ8Q6380mhtUp(+HvDk;%! zFw=rduccW1%`E}RxbCv^w&TLx-M!l1y*zg1Y-^Ae?imlPr|=Kn6p&OmV1)pPBEk-} zuB)u0A(x!rreI{#@a4;^s4M}!?KWIfPp^F&lZpSHjv zL5+$=p8FZ|4=(-yZq_#FZxIVXa8|psmci&lXL%(h1|u=A7SJaTw>BN4uM|ciVyGB2 zQ+G(D?pW}5}^4CvxLF!5%8KyQc5a8%z9scO&1tOx?ac@kM zZ(Y0gsOU~lZ@=T#+RsO|`**LtPSro&7$2GbH@6O4>$SP*T%|2XZ#{hUd!4cr{>>ae z!u%K0H*4^kh^^0r(zERRNpOfG4Jl{YK{>hCqm^o{^Xscw!m)7?(`@R zIB(hh*R^YH9%>Ug2CWklT5PIVx_QYV8hR0pOe#XDfLVK$0z>trKi2m|MLO=^Y1^e|Bt2m^Y_+jEe4#*@d?Dr)~Q>-s!-{UZWCsfWPz9XXl=k@>f-xtVz&!7kvcBveG?V;B+ z1H5U}SwngnDEoR=u!8?O@1MW9#7<;1C^w2ul97{*(~%59EwspG1hPY)^jXQtcdUMR zW6jC?NEf_5mC;b%SftJQK!RW$Hc2$TdRwKyE}AKU657kA0s#o;t# zq)2YvzBN6qNvsw#o$l}_0~i{5v0pMl|N4K(&VSvH$dwL+v~qba=7;oMHMY2t#v+oR z*TUO#PFoBMwp4A069zf{?rlUwKFe*fKqf^KaO zF4+9tRbu|fuK(vb5`VYO|3AC<qIW(f47xAc`cswMc|{_cLA<2*i#Xi}+ z{*7on&G;aD9(ND1`yN`X({LkL%udHCSIF9VByk$s!`+`aW1G~z=CHMPfhhHV+N3{^ zX6SAhIbGO2=b9ulf*JK1V?p%Sj4?^;qH&Az;i*0x2RBdR=vqDpJyAN#-)!mE56t)t zz`IIb8GIJIIeL^#`Y;|y=hgW6fKFkMD*~H`$hvLl_Kh#{C4KqLWxY+NnHEwEoFqn zll4ibD&1Q*?mSNG2MAkilb73-D_5YpHt%{AdM$W(DywE(&XMvAVm_~@Iy%_-`4vHY z{X*c<>p0D)PhU^f**}DLz^R(Pzqr9PEUV-!swdAzH4r7?ycLmi+6Y3Cb9Uu z6t@sC6RAWtoU-S~50|u>ek;vq&w~99uyD!lZa0xbwW0Xjygce_*RoGfPnRoRn3D4X(L%?2Q@rG>>EoR1@*wjFHESPG?qLk5P0Pg+e0$< zzW#%66HEu3nSUK3iNOQAPFm-)Ig+S*yfR_x2lS^sys0P)&3NZ{zqk8p_F1<@m()JB zs#y;3C5Wd5mD>*77?-*5)V%hP%qD~sKPr1L$JNd&;5voq>FX;SF7mEqYC2o*Ma&;a z;|5N$R!2phe>ezjbm+_uP5WL=^^vk(eLqR{GM35b(3Mc_AQu-+_4E66I=YfQ`44IU zM5TFL>{Hozy4y<)0FXaTcBg9w6=FM;le53PCW7r8tyFyO>Ui*ixY?Wbv^$00*Q`NRz-61Z9vG86tGY1p}H7AN`5;# zlDn-TH?BR0Ip}ofU35$;pxqP$N+Gzfz}0*8LtypdPLbt%JTK?M0iFbiYd_pzI| z)(NW*+aG@MLXc)UMTw1o-D+NP5kr{Hh}ENUfRr)Tye44A~)WVj0yKY}4=gNK_8YpW-&}Q+{TjpEiL? zFH@ypO@M}kut#&Z9`|roc%}6hZa6o{fWn^ClkSK5t$w&C880-EnLJE_3ItVYBj7#r zZzGZFC#gzotSl_W1_xUaF)=zWFW=qp-WtO=w|b84{m1GL3E-(WH5E*&pE;WD)*b$^ zNT%e`ThO3;7RM4~|N2-CuXFPAFUS7rT$COhA?}=1-Rq{c{uw^4SM!q`_>;Cf#I@@V zb)}(rkOPV9G%3Ae&ZGX)hv0W@ykIg3`rCbeN$kVVhA0A?;SUD~9iq(oV2J5ITX+sIG` zx!YbUH{~GoM@hsAc%AxSC#v$~69nSA^Yy5tii5vBEqK7{OlP}PGw)R?}E1eKj!94a&mG&!YXO)GeK1xD1RkK$0Lkrg|B5a5QsOXwL(D7_E8ybQVt*yD?egoI_RXX1Avq6cRErzaw6OcP7gTf6Su}DbtEhIa)oR-}X zYv5U~gg;+hphBBmwD8_#GIH(b!r@6==Ji@^K7~;ME!1D=y3hq+Jd?^{>z=Wa*!;de zC7na1*i!<*2+I}PZw2?nyoJOW#)Je13xGwOsY|20SO8x2TroN4C>qsUsRGoX3>sWb z7RMdRK~^|(vZ!XE-7oBv)}`l7UfUIV6vfjA8}i@?9eaUGmKh(q*!rb6 zf=Wq^VQzdEaoA(Qho+Nv!xX+UgWw!0?+ixkdwSm&`BVa|3ZMIgJXvd`wzQOHA3v=xT#~|j^bKDi< zRgY=x%Q4YfM#Mry58D7LqynaMXat<#1!>S(F#IIh<;x#gTF+lK|jwr z`99kTrJ;~P=VDzPJUr-9ZMa?S`9}A{rInQ;%yxrpnCTCz8g_Q}ZtC<5QTxOI7z|J| zFO(ZLYG}wICQr7SRW;;zZ5EDk6+2BHSdPe!T+j~+JJDtYfk0jyj%JBHla-~Wqhp6C zsH%EvP(X@o+w59j#ewyDHaR!&@%JN9k(tY4NF&6;2HQhx>hVQ z3+e3YG6+g@jXcI-%ZY-d^&V5ZMuQ<01t(tgwk)$XvfbLgL{!H>(dWe+PHV=uPuYsn-lIc{6Fpyxmy+qF`%AY$JmGCM#qcCZfY2#)gDd-_YQahg2Au_1TDSxX)?4-_ z+WyRzQBYPc|717U=3aDMSj1_$ZCu~;6c`0}?aZ~HPP%pQkEuEBc`z!Q8s*Wq< zJsykE3F@+w=&%G6Xa6W)^xj+|Pm;u2SSu`GtVop^GBS`Mo&bIExuUG4W!PO@TlX;y zfRYokm9x`)Sb_< zm^wk5X-z#qJsgnSmO$@){P`-1QFy@IB(g(eqYgk=qvk})u>;88&vYIMJ$N7scympJ zXmJ(@NHc@#?~&}UA%ZSLcR}8rosUBSw4kgYAPIN_;LLPe)vSGGl4OBM6ZYc*jq1vN zN&jYd7%Nt<=rRe~ivsTEACeHLVK4xsdIcq=c$U_q<+P@tWhId4qL4l?EZ409p?PR* zew`~dx(W|!8gAY4kx)?#lq|~6UoD_Dkyv;Aj}0uq6MhHy?%Eo&+*lqZRM=+J`n8YQ zvpKCOq3rV0a$-KZLNU%MOq2#dwk@wjrDWI>I;R8+w7S``W8fv z`*IIF+HpajSoFBGH3Lo%v|4~#l%GGNe`GCJaG5Ub!YP48n?-@j+(Qqu0!Ebh?L<^*NnV(Z zU++jlN(B&^OD|&!l`{Ok|6;i#2~-uS)j)i=)=uU%5IUS}Ax)NL zxAj0&(e&$hgB1eE>FZ}d9smhIfmCgNg;8@;=M$7V>o9!EA3B&RDH!;^L+4 zkSQ=C*0wXlyJ_o{+he_QC~j>AgOhmtxNP98Tp!`)@5D#0++#p60B{h3m)787qN=-IMhgXk2N^)Pdp!=)$Zd53}L0KoYp?e>k%uRU(TY z+t<`mBuy^*hE5qYr~-3ZY~Uh{qu7R1zPVDJ;>g$i(e(DN7RT3*_vpeL!jyl_UMJfj z(>8pXeoTu;N09U)M7<+NBB^@r6{V}JTrr@=R=-bLpMrCH+y62492~Jp{ZMm8)1v4r z$7Wm2e!C^K`!g}p%!4J*M$D;ZR>-nMGgP?rj?=u@up^D_16qa{UC79ON!8f;#l=U-oAYw-sV3nfitr*n>)jt-m!WQnoTf-VhV7 z54B}@v2BS}>`8n={2)pVdiA=+K?u!L7mG6cgfdgtMZ_1O%!ZS)+5j(8`EWXNnlt1S zhi4F6pTBDN!L#99;)F`BKB9Cu@G$T!P&?u276B>lvEnGG`6(Qe<@dC{uK{%V`Z&P& zBv<}!k$()+!{*dEh-jdvv_}g7`zYoImB|o9(w^Ay{WclOuKr16Z5bQ7z-<_ z91r@&Nvl>&}c?jJ9I#C2ip*N}g_ zmiqX~XrsI5`}e~i#J9D;z+x8LjYKXGt3_oFmaL}QS9)Gdq~)r36qN~xh#_lh zWHIm5n!T_f^JsXU8A;1C4ixXsL&MAcm|jI;BOU6uRT!)vw)|lK*TbylDd6P@W(S!V zJ+U;?M({++<#98~5S7!BGif0Tw@e0G^FvL}Rb%(@{>*Fb-p1-cd((yG$dDYbrELe= z#hQb%8{5T+62Ykl4<3wFs`3~~!Xko$FR*Ht?FHhqV%FCgwrpa}vp(2y8sl*F(yOtN z14%5@N%7aD?DJ{po}T#b@F5&W^}|eywYBn_yf^McK<0!DU>Ww-BiuS2@75I*73^w# z$sBw}q@+{=+u8)DHpnV29-eS!6WiOt0_+B;GjWnyGLYEa-CdA~xeHWj8|=gmkWu?Z zY7$M4c9(XqURHaqo7@JB;&SEydwFfG*51mXx1@B?_sd6j@J!YGPt+O4E4?Kvl^R|! zmOhS8Ok9Slmw9>sFXS0Wmzv~_Ol;+|0LuFmuBCcP*Uzs>Lqo&R-Hroo6x7No@Y6-i zNamtH8ZQ0(D>onZSlp)P z^C&j>eTvdC{sg=r(DRO^G4wkW?XK)0VLT}AwW(%9gmKuH$EFUeDCoqVC( ztv>u=mJ9^3i#OG^SUA?^%?g@q(b`TtYr({q`4RH z@~U-rcLS!Ccxp-JK8SFjk4?A%k3OIQU`znR`a`E4l8xLDuXU~j8F-kOcSW7^95+Nk z1S_DU#`);c563=0xzh3jcw*E7cBWa!9j<+06V+S~yacW^VSn-mY;3G+?V|3}Q+@|q zSh7qB;Oh%=-2DSrGC5<8&o^hh18(A&yLQm5YqQN|b+xr>iBBi}e_SFambqwg`7+s~ zo=b91!^lZ{KIWW0#eZy-ne>mJsqV44$~D8U95=^oTS)<-RUF&i+fn23o@+iacK2>P zJZ#%*1XDN)UcP+!^XD59kJX9muX(y+gpUqf+}zB*|M*1Ypo^Tn`}pB4V?)FAJ3_>u zQC<>>jZUQ@yVj52`)i|@hy|kLWe?lKPn{>L1JH^(@I4C33zw)l_Uz%*)bv+x;SQew zZ1wy1PQHUK<}7wysP2mxrUJJFj&v#C8=yfZ(CrO@Sb6~4h=_;|f0iL>Ecb+d6cQ9n zz+&o2e$LMagEp66h5GyZ*JkQ@fH>W4dF`?glmeOwfk0`%{ixA>Wp$$DCPzZV6vCe2 zuOR?ukxxMfO07T-$woh%UE2wa8ojsFo75PfcmB9ci{r>|*TQ#ln-lucX+H9EQBl#U zp8ChtRh#9$Orzqu;JcUqm!B5n>UNtQy7za2+){gYbRMm!CF)loepm5Cg)~0k2<4cI zusPn%spS zZwFr`3T%^V5D|Ajn=i0tYR+tpvPa9lR8sIObX+V%EeAD-XiS7PuXljuKH$B+e*iO7 z^W-s}-rhwo1~cWmKBdM!b8-1BgR0oXZ+mRW@63l?aRB>&qSgPjEuc|`7s5MMs>~qT z;(ELYQHX!^rY7zt9Ua|EvWs3`Ui)j9q<8NwpVd3id2Wn-LLcudMoLObC|TBp&6!s5!4va3cFm&Pk1WAF#um7B z6i;sRxgIC9EZe@`$-xlWHncrl<9GP`o^-$gqq@2}`E8MFL}27#MAGB;<`s7xQur)g z4t^8e6=MqnEv9eWzWojc6`|SBq97}{5z6w*ph*Bwt)5i5x^B+Z$GBS$wv8;S1+6Uw3%6Ue!Vm{!?xg(@*JDhJ(_^D(xS@(Q9>4fwfP%c*y zDyA1WDk`{@(gdPMW{odl`uqA8Axe*Hf|3V!;)2k%d}Rr59G`(0jBzl6!%fa?hUK2NE)YnIu*#c_tdyrC+XKDIfIY zmJY3T2r@>$eEBTIr87V@^v8=B<*kL@Cs~?EV@D!3<%|xjxM_eW-aSC^HG_o|8NzIA!I`_ zZ}^8?Gm~_*+0lqEkzun}i0K0*+DKoc(*+V6p#4MLt%*gyMsD6LGDuRXwHbfzWH8kQ z|ADF0y7$q!D3BHT3tbV;Yc`E$VPWYxp_ld(=}#3{T;0(O5cLgHzoz^sIVy^RDKqo6 z)FZF?Lsy~-qgI82o&;i>$!b>n@v6sbd(~155 zM)f$h-EhLfilZK%1wIQ5m_7}F(TN92z;!Aff1c_IiUQw-7}7gV@? z%a%DNEdb6dfcw9{2q9hATznf|^biV##_t>mPpBl=p%(X^oWKIj?gyypPHq=eyxxbI zZq6clZ8Dpi{oicp=^2>g#$;`r@Ob?F*Z2$Uw<08vAAualHwG8eM-_fCvASm5wl&kN zS!XCp)EUj7^UA-b>;Mfka3!0{%+vTF?$g zK79u5jQH(F8(htg&Y14pIW-eMzA#p5g!zexjJyh>>SeS^kYF7ltgX zs>h%I#%4IpsFi53h4}n0rXIKer~J0Vx#M>4p=e%3+X*KZg_`qRwZ4DrdSSCIOG&d&25AoFbM|ln}2t3bUX}P zzQqA2^IBF_S%SSg5>(X$DWCiGq+4C!33lKc*)&cQ~|17TpXC7=lZ+ zO&8YZZXb5jPB))_DUN#k{k!V}o2iC3-Hy8Yy1$P;%h-5|)j0_>ks96B&xmdS#V_f1 zfVPey#hY#g7bxA9OqZuv@4;I29shFfjw+59-S%yIU+Vfmic#sLu3V|Y-pMICtC9+y z$Ys%)-niML{gFKl9$9nRHcnpJgT5(mYBeuWQ(K#_7Z;}Gn|D7XEV_?>XnbMDdv$IF z7V_Ej|6%W~!m94xu2E0{1px&GX+gTX8wKg^mhO&4Ef5iF#F>z?Mv;a z&)1VW;|>K695yJ|H}I}(Ei=~k4$e28(XMZ{T`cwb3s0YS|7fYQ8O4!~`8{Nrc(fu| zvp+{ux`NnlPbG+TavnnqupWQ<%~q74M6Vf3bSf}74)cJ9JrXGX#oRNyfJ3^wQkeE`$V?eg=8!D=x(6%^HK$gpl1|FCqWtlj5#23%`5bG zb*hnWg2zhc8zHGLu`Fkb2+*Y|$jJj#^AwQ5Csq5LJpsO$vPXDKtCPmY#;Jmk7|^Hy zR~sv(R$;KHUodEdfhkU>oekv%*G&p+%T~u*L2y8?{b>)btFKpX_Hx_evhd1vO1bT>gY(WJW)Keeh7ul)h#lzTpBmipj&4+Pg+E}pjC`mPF0@WLPf`_kFJeP zeaIgW#9QaFZ;(FgnU3yo=T>b!x!%vSXlhe(2BN+rz8XUH^H0vmcz@hBdt(ho^6P-uBa* zsz<89$BBiUKY=jCWLxkYrPS13*xTDbLPb?+@^t#yj(I2be!k(7r5G$V6Y9bY^DBW8 z3k(bly$tkQ8#jKiC*7YfjV|gRWTh3>e*`={M|+g1LJwRUm%3wV4xM2#RM3d|21uon zFV4T!Ly!JsybFzA?K2(d(0k4Kmpj3PWyN7%(cc8AqbS5Ur$RjD1w`%bXe{Luj2x7; zG zY~{p)K5?MO_hlh41xm?DY`*K*sI#7p5(2_ErvBIbeI$3ZQ4%%;p1|sXwzW0kr2QvH;3EMDa9YEXqB;BD zQob2{{-_66$tOOC!@)>Z4wK2;#k#-A8PgQHmKQb3M&EjFn;n7rVrE9h&-Y};EpTm7 zj#xI(i%*1>496>UC2!z>Rq!MF+Jk(ou~l!PqDb?Ko=5o(v{KOq0<_p#>gj`}jP-enU~`wvB~{hnEP79z-!HvM4BU{D1wj z!0^T*DH1_r=Oo-)Xzm)fnWO_f8z|jZ*%HBN&;~?Q`Lyk1^BG)r-^9)tLB+T|De*Y+ z%Xje$=Qu`X;vALkAAX(C+R14VpyizpldfxQ%zB8&d_iXBbK>xO^iKuq$rh3$&yFxQ zsW9iv!1#8S}Q zAk8&XOyw(m2E9jpeSHHSf zZdsGYfHGW%NmJzE#$>BVkIx?V=H{j#2!KNZA_NFfSO9r}B{2PIx$?R`XUmaIpr)W$ zMmfG-eAC#lc?~(D#eEj#Fbg`|;YLL=Bro3s$G@7s`0zw%{VZ^|qlTfYI z+3uB?ML;~ zJ`@SGyFNdnXnDn*SUa=HU2o_SnPH(N&cyvyGjz?M$1N;N|7h}yOubJ?CZu%WMdO(E2UgCUlt^Ga|I08 zmEYrp6dg>j#6AmEmVR8ozsZ zsPB!%dewz;UyiN!6Fy{J56!CD9|^qvG~cMW7%9y4sxd!`63m9kT2Bn2VW*{~-EYF& z_6z1iiwi;xy`|aN8AEp~gAg2Cld_v`!-#l>-V4u!ZyhP}QTW-1mhI6>cY1 zz0ln4#L1;?7psHah-wKznh11l7qr+N&akn{ni}!EQG%d(u)UmH_sEHh+q1iDX%Y)S z;z9Jca5En{8H`>%rv1g&uU{n}PDPRm%U#tjJjgW8mb^e6;!H~F%dlvW+X%cmqth)( z?e-t2v741E%EBijQ`FNV9#Bv?%)I`nSLYc`I1UGpPrhm?sT39oyVe459)NhAl~^ch zS}+*4G!a{dhJ|rH&M+Ibu4dG(0tl95c3WTHQyP4H&~$~*NQ4q{-Io;#BIbq9IUfy>(aV(^V#KFN)h=QWrs6YZS*t=Qm*`Joc+3lPK<-gOJEqEJ63hR$KSwbR^Px|5$h%N;sUOdB@8B{~)YeZ*zd!Tbb04j1-h_?C)c+S7Ny6~sS z{skEVo6zk=RfF6z$Fj7-`*puVU*=y1ls^VC2=};X&9(z&R5GLDAP%CMj>w-|XAfc9 zb^~vRY^D)xyL5nvV!6Qr)Gk7uI2d%_ft=p!l$l%^S?p(}v2x7y|+W zh4$UH`+B%KR~C>#J2Q}J{3d)IRHrSkuDE&=uET0I>5x#5cded@URm$pJ445$?0$aVyyo2|Ds0Lk6FCQ4E?A85r8-^Ta(a*SZYgef9oV*c?AVgYE;iT@ERS&2 zPB4+Kd(R0U4N3aMt9CGPb4Nse4r8sqVd<1JJw`}a#%^DF&Ns&+SKa!0k5p)zq^3TF z^M23CDK~F7*C^7HYM$ZBe{RaZn;^7PW0j2>$7nF;m_+$^au_#N07||nd`xsqKo5%< z8{-_a<^lkZ>2IkUkXHWS7{9-y8SUKtRIf^h31fZWyH9Wkm*Vre6WycDZgGSq6{%IG0`c@K(plmyV7=Z^-efG53+{-c(vrU zW|V-(-tTcq;8x552#Y$u8-B!K+Dg{ftpIT>Sy@?~T00xo!AdPQQE0mB*4(sZ+QX~U z^7914I`1=Pf4%uqswoZvd7XWlzE@o@9gfbUF0cd ztgrW{pxW)apwI%*yO|ORoybhWfl_*W^5xIk3x}caR;q=Q`QQ}@RQ^KAPak48Tk{+T z=L4$<2S;pK8K5F4XqCS`+DlLW6cp6m3mnGok^?|-Q24dS6{v9sDn<5Nu1c;CJgx>v zzzD6}Q9P5?NF5u%#V`T~B1JNzGK>c)jth=d z{AClEUl!bito0J`000)?Y4Rz~;2A7@d^kjkFd z8_~p~549T2F@?NNmd{=?yyCDY&cyC}@M*3tAUGI}xjub6PKIi&2lnvw9zpuizZNHA z=ZKz?FS!%uQTfvAV$QtyK!e>;&yE?&lbfhs-r5*CxI3~2ukyX}HOKDE0j|Gz#h51i}w3wOUJjQoHHst}8sX z(|r|W6$d$dP%j@)jnP8a+d{m?RqNibG7O@}Ly8`9KG_R(P~lN69aa}9R96O9s$ z6UTeg;cE<&M32fsWD3;SO?p0wSBLu?PrdfBG_hks=z)&H-0#k%Wm_?sJNTW*AZn&n z_9X;$h}&VXb!Z@tV6k_$yryR4?zNxB?;>#{zWVvnUd^PZ8D=ShD*_XDEsvyQ$^Z_b z){QdA_q8#~~XAYK#L7p?ery7dYGzMD*uCtTjx zFyd@o%pCj@=JN=r%5MJS3>W-iXIAFUWWd^k)e*P)+4GCb7a)vBL7}s=^RvNztHjj= z(<-Z;jt-%&$DZ+U%o=JAxd;(oz<94;8>m|VW+hgvfk+~J={z+({bzIY$p#rQ)>`2V zkd==ai{>b zndkX;`ICh@sTXMUFC;b(3=IWw{vRzs^c$A%cT_TQQF;3d^{N{u8l^h#T5oUuo*q8< zF%1mh0ZdbCuMc*e$r1yzo68eZbMuxczuT0lnHiOAFSpoUfZ)h8W3)I1fRnMi6q`&~ zF1U8#=e!U?V%?MU<)%xm{UbE98!dRFF{4J6*GwYN|UeyoZ>X63$@Ck)g)(rU{ok-_J)_5ut6gUO$Bdc-rj6gUTJ1@A?B_i9 zb{~6z&ct@MA}At_KTC;rW44s6#NPh!u-axqaMay!;zX}Ng(}1hr8JUE#NKiLfvL^rNP&Zu&m){q@-;dPgH;R z`P_;>dimht>gK4$Ky%#BSJNt^Bk7R&id(ZpVMtS|fV8aa%F(bPn|CKA_(G-kI7cma z@wJP)CQ6T1+ZXm%mW&3$ss5UdE=lO>sAS=!3A{T;ei!|;8>f5FxY@cUZthuOvjw1tnTJ*+QGQ-F;OBY4SrM0xz)Gl?ax$l-0fLRwn+JdAs9N|4g zFgesA0~)6%oiEXmK?!1RZtj1}FXS^;Cps!JZ3FbkuY8iM)fbn353hU@^88Kn@QR?i z-u+D+J&d7;2Ck6zhK;V9H7_nL|rwCdcP9HQ#(iPE#Hy2gfQTFgkLDpbm=gwIGp zb+1_xytKJDa7>3=R#Ek9W(FNpJ$+1!Pnlx|KR>0UIDAf{kOo$M>gJ9fbwBRK|4ZZK-sPPUS+%bx~~xc))&+c?ZP)NK+GBhvUlV;m6t*I zDVkL3UAZYh6?Y+?o<=8X6ap+cH`hvZuuE$_h?>AyJbk24*M}h4$X%OXbj*X_DVfll5u%lt2+9p?)5^ zq{*w@%E`;;$R)8KZhtCb(F4E#(?XrtwDTJ6+FDRX1{ol|9vrKWL^Nk2-uw@hIl+sV zPckaT#Z^E1M_xj`q1JWdKW0lf>7_3`Joa>;u-QFB)2s7Oo;C4ku-L*V<^7VK_AhGV;Q<&`LrAU9$=eU=Jmu`Qs zUH7?=iZx_MJ263fk9Ishn@;1CJGu#qZ7n}LGc)N6XS20Quv>ii`JKSj)bv%c?piqd zVHG^oC3*l;S2x9Cr`$l#=Rvx5Qu+^3Xo*2^WxKp-(c;9cKtTLKwXDZTQRw9bcXY zVJ(|B65xni%b;saOaSlsER})-FjCe2eiDq_jrCWtQv7UR+tLJ5rnwg1#`oKcExsuk z>sun0f`Wqg!~>Y`0gNO-_$=oexZs+le&oJ4weRKROebm=9}MNl;uo5sU?6U;IluuB zKO0WUfeHX|99&!gdb7?9)K^**BynvBw=PY9u9Me!L#f4=UZaUO3ipMhiwBSP1H7=_ z9(q_OHUca@&c&M~l?qd7Fo39)Ujw{xdnt0^6WBDt00()# zms>p!&Ia7h?sg?o{7io+G$LaCwD=pez7X#PC7L$IN~uI}U5wOJHEoGO%k|vsvoNNs zT^`9WLaN19E<()hi4vyc%hD|`1!?(?o|$A|leem?Kg_XR`Q$0wrNpo`oo%HvD~j^c z_dHARE>iyDZshC#aL6&EznFsC(%I{$iUvEYxt4sXO=B^SL8pvj6~fb?Hm>;6FVepc z=P9$^rS{W557e9P>+XJzhUUGzg3AgWAWLUi2P=KZ-0vudSa@{Pb!VnB!!W+&H8*nv z*!V~4l!f@K?97y4yV`4{irl4ziG_{8hX$* z#>U4h%ue(p^LpP>%zEs;0Tcs`V*0_VG$T+9LE0=Ra6V_O1qz+zm6cG?h4A_vQp%@r zie)JcWg1J+tNoV7-=C{LnijZ;0}a&HSTTlZta*P3v1sq^aJE#DPCX7lYa}BH0zp2L zUwvW1daowG&T@g5l9yO05Ve|fUcvdB?tP%r-B!#8Db8X6j% zYU_7KLFmr2bDWTC=OD@KKfqJ=B2$w^O(us}Ls~}WL;K~)T%FSj3h6ksE3MJvVuj$N z_e**W#*udS;L}~YwKetC8$r9sSX?Nn4T_79YsRz`!FQQ8gTt z!XnER%a%+P@L*o_gVb$9qu;_uop>~d`1$yffSwpP?t}pS@|n2-Ocdo`kBaj%rA$#e zx-zcHtg2${+tb@PH%8ef+c8=NMNyRR0@^r8_+9y#tdguKEht;RrMK$o6vr#C&{Vp2 ze1I5CBS96?gnLh-lpyH}dD%zazM*PajBIRAhBQ+!81Y{ZW(Yu^08k>@Fr)EVUp+?K z&iozz;$Z6G9C=llH}(O&It~&P+WJ7}P6$9jv3b?}B#mb`R%IXyhbSLtNkHFcUd__$ z(0ctPxRD5nfDltQ0r*Sa5CS6GMuSn3TpEx^=875b_mL&SXY^Z~(*{Upke}h=%4S4< zZa!#P?Vw`_MRt6%P2BHWqGPTu#qj>`hxEhaY9by(rG10us1&{++M*0{=wZW^6xt9{ zi9uuAz76+Kwl_ZLATI2N_52L#K7~VV5#WO_^6$_NUoW&vMONB%?fHm=_+3Mui`=HF zl6-S&zZ@&j4!5dwF3dGU8)7zW9%y+8{e#$+IAbQcuB!h&Umi`cplS-gP!^Ou>*l)nAFN}Y&x-||51va>Zzeguj zuV4E|MLo-QPviHX-Wec)lzD=7hHi7sOv#B(7kK3pH}#gxP`?7M@Qpr^bi3QKT?%Pa ztqx@2L_E0~8fKM%)B%l=LzR7!KT=Nu3rnjzkmy-eniEg8w*I~;tE`Nbh>1)`Ptga6 zuL(r0bqmZNrXyFCn{=+$w*oRr3GQx(BrYy~aB}i4N5$!EPam9JV5buYFYCb%EV*u#=FT}#~ahCSeZaJ!EHBZS>X%1KFo!?^K;J{`)TpecqXSo^zhZS&T5-f zVVBPUUw#;so<2~^VV+6%T8{Dh1|ZRrqSF;9Jd8ia88i(<9X`z^*V;sa84n7`}octLEql%$?uGAT6LK9*zl=JrMvhz4REbQq$UIi`)f*)h6Mx2BEUp-K;v+4N2p?9UgvK|=ZP^11 z^6%Bw$+1r@ylId*ZGqiua6td}MF7~Jq;PO!4Z1xSKBSX6QwjNR^BlKvfMN`EI>f>@ z!$nWLPZ9bDKRRa!6(dufA`mR=cY!yh9RF7d4jLQ4^JJsCU2Go!#738;uQJElfJ4^r zDh(wRigFz28jQ6yIDyIVN-(pQ_^Eb{&&!q8bb}4MhWh#n)7vw-WY(AorbW}ajln}( z0gFq@nJVjnnU|p#y9s0khP8MieY$UhJ-nQFc5aVI=33(bRMuv+A=K!BU#9@r%VA~Z z!|nKDo#v>Pxzq?!zs@8u~MdJ3q*i+=>nLWV6l=UQ?U%XM^gdJrou&E7|PG*|6i zs3_I5r(j29$b|+;&qxVv9n&0nHso%(Yd|aPfpiGn|miv(d!@$SKa}?p&c~ z`c8JOqQGYua4%{6kQjhs{w|%Zwvz=zL5F&G>V|{-X(d1I2$r|ELyL=xO zy76F+m#`16+Q=R5aw8h>?C9BoW!2l%V=5#p zzO3mi$qIA1K?M;c3)W5!;mPF6k*Ep8Ji@| zVK(5#t9fUyN$D;GiSS4AiSAT6LtamW{)CaH{{==und`-txLwz1d)kxi;QPa~=NyLm znvW_-$or0S+JIbI5Btfx6m_oN+A3_-R?@B-%+}F5;~NX1RA@609rcU?0_xBo@$uSi zBTA4&3y^1tUZg49wowjsW<7C#;9qesOn*NUzFWqimQVUgq+TdEdFrt@WqiOdBqX#L zLe5iJ2n7&3P*bLY`Qp(6ldcD6xITX#AY9a^3$@{RZzLm3x2mjw964&aBlKZZT-+}( z69A-Yzka2oj;5pC}k%Gt^X6Y<62}dqF|SAl$Ls zK0fRf-+hS(0h4VcfU}EVteyM$^JgNr8$E|b-%{tv_Jw_;^IFeSLczpR8pT|&y6fsU zdr|MMM-_jWtGiZJSDOK7GLBAl9fcHjKmnuG#=*vp#6OJ8%cG%^jlXWh-Xt3w9*$Q0 z{4^SF{$NMM(O=~DJP=IERkCQl>~1*OVmn`n1PCY3%)qv%P_6x_WV&$bHbDs}0*|j> z=gBzj4Hkp(rJ>v6+H^?9ze^ySNMFzb*h)q^D6J>{Fk;QK)|F@B#5+=F&yCJZ7oCVh zH!cR}Yg1ReIojX|$M~nbPnBnF_IQh0zxIN?>u{iHuiTzm7hk?4)&2wfdjx}PYU}76 zj(Uu4UbT6H1~GQG(bX%oR$%>F>0+fz8FjplS|RBxGehkXkTez*7!tfR?AFL}e&=p) z(7!Pl{HhPAHR9nW;6uP5Lt*!czP|o}qX)xk^!l#-lrO0H7+OhBouILN7@!1>XSqn? z>ct;k?$^AMST8SkLVelpcRAqt=MCtE)U?G4Y(Y!nGnLmRySteGH+J<*KSzblqH|Kx zZk<_+UU3qx8d~Du@!Q3FzR}(EZXld9Uz1^w8wnUk8XYEN%QIhXC}7ZR>Xc`#k_i zpMwLQHNWeIXm32zbgc(V0<->yjWXkVYRnN(QWu2xMI%@lT_Yng;N&s_^69sdl7zdX zz)yF{Kn+m#sg;HEX?(sj`Mv-Huu@t)4pwy+k^6gi_zg%4&abYB<_t|fm}s9|?F;&x zvktm(u(NBG86mrEjb*8CPXO7La*-Ar@X?Uzs$ELDDg6~4|aN$MysjUQhQC{7433aZ)09u zd>%M<1wi-M%u~|5ch2OO6g1xyaA2hJlDWxp>}wS4rEu>vGDk2=0caAWW0RlWXB!Ip zr1OB1rQBI~s1l>f%EQw>Sju@?kFEtir_;xbcVOSx^U?O*l+2{UCD}!Z_M0zE+X&0>5YLA=_AMJ(mwzeS036_oery`=TNH z{wOfM;SVFauu8uO`P0dj}b z8GaoHIb#4YQL_>P`!jMsKk+3feu=y*0$$T zewNpJ?*SN_8Y?mg!s{no+0Aj_aGHCcT)mmP-$KNd-y0zhuXA0aw-_}1wXl$m>Sm)n z)1_-48xzy>jj9)O0weN^DPqsJ%4(c=2#un)NO!<6IMVs=Ay-qgj7pyQGqY>wPFzWm zuf`pP)q~d_A)%28c`svropIzn0|Wpj(OC9krXpaO^oGA9+-}8L!JWJ(36SpCTWn{~ z1<12%-%CqpX|XASsi@mpl-2YuKtXKJR|trD<(QC z%X>27>CRbz7@4@kg8BN{QQdQ#kl5HapbnDK6MwMwz`0P=RI~qqFI^7=18TPL({S^` zK-25Y2OluHfQ~J*Tp&ipy7;-2dt-QZ^6Tqh(_R8D^{f6A5vX6lw%tr!G&lj0_3%hY z`cwH_KvSF*)+z#aN}v~p3>~!ydCQe}94v`}d19@lmdeovunFIfA_j*On&=! zxr0u-g0j54{L|;p5AG#|Q*0)O`gfo?_7QTT0P`4N%K`ctF;7p=bmJ^=_>_B3G8YvU z9UL9CcMe310mXr!rzjYI?sj{WpYf94#o3iJF(WzVXGEF+pZEGxAQ4^ZU+*t3EBgVq zJUk}dkqUK&3u8uV>d%Yu>)-paNqPMvYk7d|8f;yEyQ9b%^(#Kj);gG2S#>Y91u9#Z zzS`js6insbJ#KZ)9N#<#u2Er^)D8s5b}2fX>(ZM6&T*301UNlejN5R{PPb{-vYB@1u02M$@J}~#GH>E zfG?DroGs)1+!+{?18sKf;aYFD3jOBs=`09wjs#>66*+m^YK`?o(N`ed&J^(gis@A| zAdpYtad2KeFb+J()PS4)`G^8k5=n&GFYo5&=3bo-OU<=aQR)G zObP}XLlGx)K(>D2BMiM2gfVE|*KLO^XP|;~-nDM@3~GgX5Bea(n8h=24)4k|4vI$d@TdH@|Qvm&`4mLvi5X^#!@LKB`XIqeNz6FuwpFKi=X=EQsw?t#!u?Gy`RgTH!NGXQ5(+ zu0f1==c5U`Y%?I&?d)y~;7Q?*sXhApLs*n7cJ}gUNLk#0&>jIe%5tlI2e8MR*n=~d%BQ|STsZ@a}&^lF?SgCGkm+~-$&nfob)o$E$K}dPG1{4altt@;s55&~R z$H(Hu_@tx?o}T9%`#fA+)$hmu0I71l&&46f{(fs`*X-=;$MImBx%ItzS}02*AXJA`<6oh%smgzDqqT{qmEt7Z@zF9g(m*?`yk_lk;L)8pf> zQ@@Q{Kf%Jv6>I;D^j7lit4Ai4z;X!J_*)F2e`#r{@)xXQgte(DWqNu#sIV%40?A&l z)nG~_61iBT7dT8!xA+QhS&!WdHtXu@%3Qi4K5E`#(-S@O4-Piw8UR%8nr^`Gc*nc+ z7M}}t8;#O<29gr37Qjz#4b|uifH494y*al1j}gj zGbXW+Poh1TKm!FC(#VLxF1|ms0sw^rsY1Gr?RizU!^=r62e7|qEQkC*L$k)M*Usbs z+;_d9?7lMRl9xO2iVnMb%&PLE75~yniPtG7Sk)U( zVgZC|{L6w2SE*BxRMt%9Acw(+%Zys_kdBhwYR2|opH9_Yxq9al zeh0VB?yAx*m1ZxlY_grnP1m>YME+DpDr+@DT%I-3th}1NB^(LO__UniAOQ%+n*=nFlk5RYbd1(y=WP8wFiUxcQPREhN zb;2Ne=O+)u&Mcj)FHk{wk$2{w_Nva>TAqXt?wlEzc^qbmq?ZFE65 zU#;s!SVAASS*J%|B755gVQ4@4*2^j(`H92pPM42v9X&Rjy>5zLh>8If&f2g#bQkD{!(ElVCc&ksamEYH3+Mn zss3hPyhSYJlpGYA(A^7aJSR}|RzXtZQYNlH2i2n>DSr5HZZfODZ9Ai}B-|_mdt0o5 zd$HjZU>5`>gUNIxmYbAUj4U!D`ct}q82WDA;0en|U^q?&y=Kd*wRZxRpkD{oD>y_P zYAgd{v)^n_tQ(RZ_v0~v$+zyqyBo#y$pROPIX!*k$I$aAV3QG(ni@taeSC8up+GFq z2@by1Q!ug6J~x-z54~=?I6}zp(KNDadwTM=GU{2D_07x>!en3;N_4egaBO91<9ARf zKF~x<1ne>~GNO}oUY8aumXTv_O`?g$CP`bqe~gAr%nF=`Ja%PrLSEj_Jso68RM{=2 zt21b>Ln56V9hU+Zn)%A0&=zA=0IPiVJtY8HV#~j?B;LMzCp7AGettgGAW!6{-=Ows z2%Xh^ju$kq!PsQ$z|Z2H0TlSf)HT#QAqGeSy+6}|xwUWR{mGVY4G(75JK7e~fkp|r z%xk=mA}H++UZ_zl6C6R(J!Y9G?8|>&k!+1)0GdOiR8Jasfkf9JmY2m#v4xjTi*;=m zn)n9YOAozDbW2G=H%Ja%GGG;ni z)l{3=2zSx^o6E%GpOr_14#ax&geWr7KlO}1!-F`4xrLklT%-C$m^pHKh*K4yYyN#o zk~AY1!yfZR#`{-JV(+6aRNQnNg$8bkk}s#<=2KMa%3E0^a?k?+OR@52FGMD1`817H zT-?EVe<4tOjl!AZO2qr@<&_j~`G}@YvB+1&y%FWVtG#Q_XW+7=A((kKtxQDHJBdKe+L8Pv-nX z!Lndrur&`42JvQDBO}fodts_7hr+d!RtrT1%tS+q&+T-q7jIv@>-Yr>EL)ke;TkiN zHN!uUZ(b4;=WisYg(yy`Zg=@*y~0EVa{GLXNPFwih9E3rx$&C(#Lz>R0!8F-A{)`W zmaU2E*oumZm$;4aPRGTjggZMp;b}&K!DI+*V!!(G>dNL^Jpt|pkDno^l`0olu{CD$ zxZtt=j(l@fD0RjRtfoh5@?Vkkmdu_J!sijk+Ov|9AWt;0#y(QRe4^ZIF^@Y;GTX~_ z`x`Ee3oIIvTh>?xof|(Rllp00v^fXVif?s$Z^$AawELUAbN<`qa34ZrhsWQ)+WD6!#r zkYAZApE3iR?;9AH0Q1tmPZS>^`By)xgj<(ZD+7@ScHVzYdbrkjWfs?F6csrvCGXQd z&XphP_2^9Lt>~wZO@WGQXxrhYpx)hTAmng zE}t^Bz7w)ooWDVmu=U&ijX$a$5)e;@L`1ZVb^VuT2L9oH#QnM`L$a2zIJc#LY>GjY z6gAuX<)CSF#|I5RNB~yiDc$YcCg3;kR>-JG1%6CUyIy335p(Wu>{1!nnBO62U zW`>siTX+ePNayA`cy`HGt0K(n&$q+=!3*E9z(iEa?dc98Lq`a8CHRIz3U)=J_nvE~!hIcJkcF@id_l^$Hs+IHQY?jDgwy zDc7-pottpC9qN_Hf8GnE^Y$;E{P`Ly4h~SbWGyK}cAM5NZzv&N1Od*5vN(S$;`qq0;pYuQTmnA=OOjg0p z)88^l<@tw1iA;r_{xx=3+6cL7$g;~y^|tBUW{5@nXgLRno;lF{kK~yDeYJIKJ0TW) zR9&!SkN4j4-mth@MH*Qbbz{1O=@0c|U5}8){wca5QwL95!VI{6cgaLp-4&Sb3fJof zqhrxE9)F@|f+d`PLh{E5`iF;||M}g^c$qX>;gV0We`UC-7^;`P9QMf5b7(#je(1l* z_Yc?fBPTYeekQ73!->Xy)6qB?ogQ7?oVj(Z>j9D; z@p^NFagdfM{wY#JkG=$~YOjJwWTd*cVF{Q2?fKt7n9pDT;X**92i-sXSn&6G|B!zG z{yX)bz8_>j0$S@IzV%-}T#)7Dzr8w0KRR4i`9^buA0RF9d_w=npppOYe^udIK>|wY zidRo-YATsY2yHb{d-(#x1`z2T%dB1e`*WP>M@}azzg==MYPkR5ipS9YVNv|Qci;Zy zp|Dd@?N|ObiUMbWSj>q|%zzz-7ES6&ZSkP-0v|MEuQtO06z1}?ZF&Y7NgnnYmEM|m zd&l*o|HY5}|Gt`5O|^fB!vRS~@BTS@=EXn6h=A8k^&j48@C^#j|Idg2!)x<@Zp%NW zApQUTwxom2+)Y_YDPQ|cq;NR~r}1=iG}p3|X>_Z}9$0BAq_cOM_NdzL(dCfz51nqj z0&XO~*HRM4Jm7k2?9oA`DwMb)6vfcIb z^GiOP!2o$HlNlMFGxh_k+cX=%QGXJ|edo;_fwpDy51KgxFYoW-qsj4LGHBMj(1e+4 z`6N!|%Dr?au``tac%S``qG4&-P1inLPcYTr{|>rYaD(Zorpx6DUR?30#Fh<(R?slF zgoL24A@F;ajMW^CM4O9}(%s}PASmdMZHqMeQ^tFf5j_KZbd}RZ2SY{>uXIDo4qI*p zDlYkpprEZuxVKQ+Gt8iA+xTHEmw86upt^cP7ZPU6Btn*lry z_#y0?+Fcdft^*fj(2@j0&k5sP;UBJF_I|#Hm&1Z9eoYT@8 zmu=>3maC=4Sn4#ZO@OUfwQYxtx<;an-eA&822Ft~s{Ps0phc#VQ7@3wIfV4?ZB@yU z&wt!JB+QhK57ELZ*z>Z(k)OBT?$HUIjp)*|lny06Z+E?^eq+N0y(rzT{xI%^f`Ynn z0Tni2vCyfoh2h}Dj>En2g;N3*f#c8`7cZ2x-494~YM zRidIGzpmYUU&tx~e&)N_eikj@0i6U<+R$z< zT#*&K&o5=Z_Wj%4*uw`=+2#R%>_SbouX(-q_GEK>rD?KIMOnF^wB@#@(+P|%t5|RS z@tU24HS?v|NL(L-DS#+m%TC7fMH6fY7N2ZhF`pYd}zWRxKL+T8i> zkBqQW?J7o7o@}c^7x5z(2udSc&lO^hfBSkA+90q_YJj4ur$?swV!?u*i7A${SgYAU z{-=c7MW$c1PCLwF9a;ch^U)R}ns7}7mK`j`#`%@1k`k?S4_^{IZj3Xy9vS%-p{`yk z{2~6P9@n&^p1w zSU3Ot7NZk>_r|IWx$1MS;(q$pXk)O~=|-*s$xQF;)pCMa(_siXA(wL0J3hd7xxd z{@1(8$)<|ZO6g9xVI`i<3$lO)l*N-x9wOtTk%P)dx$-;f#JvN zF!az6yECF%$=L^1U;Y>d1YCQMEqAXaCp{zQudI16F(7VcZZ_#zQn|L? zph--7rk1Xc#3a%xx@oaHUY^F~AD?0~l4~dMf)tNJQ4!nC7T>CNVDa`kvsWm-L@WDT zNSZ4>#|@6xVA07LdGZa8LPgqY39#%iE&OwIb->s_mS0+3c@R~^%Q!tcCMFk~)L~2489#fiuJO8LPcQzv9>4AQ z{*$F50Mo-^g+O6->UQYf3f$T zQB7yv+puNSQ3S?{^qHXv2&fb(QWOOP(tB5tmQX~Rlz@uDC<02AUIYROA+&@NAUdcB zNQVRxigZW_p_c^6d$^zHzt(d*^L~5ZZx2h?Qo$s@bN1P1?`!XUU68AMu_0T}@N2{R zK9bi+1y-Z!mV6f{}ov-ZCBefO@3J(USPjGXT+>Z*N zSJ6jO^NpIAo%4g3CWlL{d^#a?O(O~!G-iXo2U&=etgK0yq>-azHuUs)DF~D-`rY@6 z1Q4~#YDx`Tr=J#EFh~KHNGvHfnGo@Tk96YKC5fk&oROb$ZGuKhBlczkfQiDF=C6g! zJF(q19YYl4^(*kFMIp{QC;d>yiK2pc)$@zrE5D5T1-9_*KD7s%7{x`!$e2{ z`|lisSt7gZUEJR&w-59<09CROM&gl#ha9>P)8WNNpl|%>Ld2h&NuscFx0d;P@uJgH zGqxBurO#d_Gi5rr+Sy(EqKsmdyd-iZiV%3d91Y*O!0Pr0}X2D$qX z73|Y&hNy!Yrgr&(6ztjZF1cln5GYZ|NSSV*P&*#xM+Oiw#CiohZJW=DeF|b z)qqQ^S}*_s>qrZr1fTn2v$6`Eo=F#lm~|q*ulLN3A%`Ic(0blwkX0iqsG41fPRdNg z9va*ZH_eQqhdNWMPbV56eDmQVBs zh57*57yEdNu(%xMl16+40$#G<6Mw144P>5VdE&B&YY zcj*SBh(!my@|zm#OSXjjRrK)YuQKFv--$n(6%9&PhIouvBqv9)WJ&3>Fc&r(sOxn+Hz7(&dE-42K+z{X@A!GD%<;_wiDxVNzb7>Lz+k z)!9t^)ht=i#%LCbjq&^d0_{{k^6hHN&9bcI_ti$UYd)mU6(Z0^9~YOxQP#s}^zaRb zYZP??U|0xLWn^tTN&RufCQGfl@36d&THoGys@`Bmsce?&+s3{Q3DYIRcus z*mS$GKB#BB+P@=o(+pw?)LOl3M=Kr(bOO?lu-N8Oku^TMI+Oa%KXf*g$HboY!Umvj+qL`wn>R?lTEGw-*`1rGcL z?FEI(84U%qz3_1Fu5Gk@TW)3LV+e7{rg!fw$csvRN6jMAM9dtsy)zXI!?C|UumIkM zZ+ga-WA&FKM=u6PyiPD;P%7XBARH)*EYVF0ROs_|vz!Dd+>excuDFl9vQkdemAU;^*7bmu-mjPL`FAPWmP|W;g{vB&EDJLgq z^vSF<2qbA>$B=ORuZ04t5BTLb^U_w9mTGfnnIadZWGfs(iN?F7O8X}uR9LP!IJe-) zm`sI5T~a}`>sa2RgDR0?6W$em21Em=(27fydtlHQi#?Zm78dj%fw?~KaxJj0$ct-L zH@<=SJZIT{baeE!dmp(BU0iIfB4Id)1^Q7XkSFiy8ka6rk9Hb6r2n^&9@q`O${)E0 zwzpNe8Riy}iK4>7KD?Y|1T{L{Y?U;luiw75sOvPe=#!z;8VH~6Ztq9KqM@@nSEuLu zFyZA#>Z49BSWRYk3K*)Lxdk*W9Pf}4sn2de(_drd5&17oX<= zne{vAZS82Ek4GbKjNk)s9J!EQasdA{gW9I-lT|hThDZDfk-; z9(LF51Dapi?V17dTO=q36-C3%72}JIFVWU|!h8DE*9fD-%z(rzwyQN-b z)GqM&=h4G-X>&qr!7ALP-)zq!B%KG;c6;16jj>w$4& zc!21zXnkGCtFDVAwoZG@6)l=By-ry{PP_d*;c=Q2J+2lZh|q?wjSLv!cYYh zv2%S#G%to5C5X0;5#9;aF$}RD1Noh^DoN?$5Ga3-EieFP0Nqs)cD^jUv0hEkr=nlx z>G{*A3!)eC+L*@OcXpGJhYqiS>@9%IfemVrF%u*^hV7~s78Ocgzhs`^52DoiQYRz) z6~=eADF}z4!b3XpZpz$KnW);@Tl2g&SdGb7iFMxb>eu-HgtQ(rU3h*29IpKgJmeAa zq@wo%BV^1BAqp|LsSWcb%Y%pj*1lmaCanCBBg=9b7(`DqroX&h&df6`9~iKVnPby~ zAhC;qR?i!*s}G=I5)K;WK3N-k>Oe7v)N^;!Q4=IPdI{j0$&Zyg=<2pL?%pW#SuosD z19;`8t9|tQ4e_i%FA@8=+w0)`b`0^~g-c4+J%X`EIfWFoM(X{Mf!|ZSOqlofW@TCU z0D8WodM4ZGmw};-hMa#C0hiR=Dy*Rbe#$c131@qXlI4C|5|HmCr7;b^71kEGe^nh!gm7l{W?eD$&e|(Drj3TQt63rswGROjJ@q zw3ZBakd%=oeMR@+LgkE%)@dW!*^b#Om-z+=KHT6Up}zjKtLSg6DJCio1FKF4R5-!6 zUrq!u_Kz>MdXr(Y@#@UJC1BId98FA6DiN&qNJHtl0a0mIZ4)!l7d`u_4ji}0#>_Jk zXYtabdAsf*QQDM>g-OGfA^}zZ8=aPgwYAoaF1Ee27220PhVy!{$4u1)+_cisO%BWW zfFJi>m*reG1P<2F_2;N!t5KhvlON0opV-)<_o!jjL)b5v?O#S`A4gx>+LQRQ zNS1*BT-xIH?U&%@86L$;ZJh5Obn+8!m~D`%fS7)tuexJ8HaaqZcjKW$$Z`Pf1OU6a zk>>K-^cO=`a48w-RMB8oKV5AQuvYXeXWGlKzHA451far9XPU>RU-Qe&?;NZC6A#OZ z9IqLEh~nOVtJg+66UR9?0jghNS!&C891e2b_C3cDHR;NZG{)MoBMQVN>0>H_b|xly zZ1~no`rfD-0%g?Y4V2w2%{?=F<>Ng%8*AEuXZcTn(%UPqQ10XttyuHr^L*72V)ut0 z_oN|D^aKeQL}mPJF#xUQx>#EB-suZ#cZ0XL6AHRjo5a)4 z@cZxfnBQXnvP7;`v~^I!l0Zz3(r*c*j(k9cY|bG;9wjC&Y^(=O7k?v|H^p`s4EyTy z5~17tMr=BLWv!QAcwb~b9Tf%bINI`29E!NF(f8;#yu2Z3nHuK92iPUyykqJ=N>rv+ zGoCtzOdGfes?Aihqa;bR4Ld|@dwbKueH)v)ChH}1lT-Drgz6O%Hk~yn5I`SZexAeo zlYjgutZUI-NL5wxju(Y(EwE=sF*T!X`xZaL!LI^_`XsO)skFi(!DLYjb8`R-s2jEb z5}IONlA(8&oRn*-Xo*dqIF!$uGrgmU=Gw^WvBJH+(X~f63LHSajzQ-Y)SGLeuHOIt zNKh^N!+YzNKQD53rspZ4ZTo}inhCG_2Ii>RUEW#M;eJUtofc^SQl6elSU-_Rh_AGF z<#BAC>-*-0VgTs5w3z{b;V9?aq(413SF5J;EvOc;SxX%M$#nsyjRBZ8BtiTQe_rvR z@-)v`=s>*;pW4cI8&fXCeuq`*%Ig#7&Yy>EB}dYUyAv6=6G|~db=)2OfaDiFY@f(n zTPPA5QS}(b=YaJ`)mEDWC?yaKv>E&d=?9oo-MVsSUHHx&JnH~kXCK7Idiy2i2Yr_G z0CDdI;^HNRTa!8t==Nh9$ab)`O{suo zC~xp_;{OibTK3!vZ^EkfEuWFt&*v@dP4hSCe*%$L=i|8pk>GUWS2r?Tu4JIs&v)JT zy6V3BRYo;TxuZ-j-vGu*V4i4EaT7-ZPRF)yk5B6T@95NlvYdGa;BkQ$_MO#ijZq83|~eIy${Y98BLh-Gc-ov z7VBM*hEtjSc_MDl4TFZSTa4h1tv{awUdQW-@5;tXLOdo_$!Fv~ZS{J*R<5XnMlpft z6K4vl2$hw7)$9Jrgxrq@z$i)^5Q&+GY0696A?^l4`;fO|3H)VAKsa(b-N1H_rfNq~ zm-p^<+3kFIDE-Atq!UAk%uCSKL|cRX-?X~t9O1Q}Vi_I#E?QzD((=O?5l2r7Q8tLz zylMx$1eQh{&O-}DOgZj0KqJd|m2SOiJ=TvikPZ&E;~4+EpjRHd>#;3`9;TIT0X)2v5Iml#|DbuIbnB! z5HmSk^&SQ3;6v-v}3_jC1=w+qF}fK}{-$f@*~% z$*9~RXZNAi%P!IRxt^2J2gpef60~2jnNH4Zw9Y)z9d)w)ZbbOP4LmM0Q6hl!T_w7V54scEXu|*>I zU~DzPNZWsZaVBCg@v68O{~>m3>KLWWr*2sl3mq*buY)R%Cuuh7IpC8p@lw775XrC5 zn-XqT=6;HuU9oNIpaCpLR{0E>H_UJvdE7Y)X7r$vOSr=rgdPBXta#b4XK1CxadHXc zn?t=2tE2AE&I+N^4YHgGEQ*(;9|hYFYp2)Zv5z=_u=*&2uQF;<+`y2we5rUCNl|PId1z(+tBu*=-5#EKa+8?2{o3 z$cW3aPHL_%9JWf^0w#5#ZaYH%3nM3i@~Z{F@K094moX z7xz`&#(5O77)<~P_yCi@pLk9Q`V6UzdXGGmL7K7$_0CXttuY~4VT|;CrSQRYNPzp8 zrGqNgf))5~Wp%k{@LjY2Kyu_-JCc(+T#?n#C2zc?TNBt9NP$tmLMEqalW2C+Ubdco zd$s$#o{MOcfH^XJrth;y34!nG~_osdmOWfK^m^R5@wo?gQ!}ll5*yg87iZ1K)`1XLy z)8%ORehqG%S#jc5_XU4P00Se{@zQlg*Igdka-4FwvUL@t^3{g4Os{e7_5ctTaS5tG zS-}7%DX~LvQO9Z!Wx|@3rT-(n|Q6g}4900LzN z--W+a85I&9IU3v4V|G83D+Bb^m0?lsq~EDg2W$^d(dP@d8H9P_U{aDcbCUx@#&Ysr z*V^oCXh&}c7+40m3)^@dpeMCX1^y^)%YAlOVYVsstdP*mS|rDRpUVRuh0Q{UR~k1q zbaAW_QoJ9`@zcE4a}Z-iN4FeNp=|#ZOF@5Wk4P5XZ%zQ_l@-Tv{CF>Lu>iL!1;iN? za{P2=`8tW`Z@0ya0G5FvWmA-H)!THIlds#(%NF~#rX~nY5(1U(6pTG)VNxZbG%vmT z9`N#@epP9GiOUZ}7s%@ByEmR4q>?BBx66@BX<)wrvJSIa_MH21F#U4ii|o*4nRBVi zmNsgXzwT>SOUyq#owxx>C~JD~sL{Mi)e69edz<4tI2Pc;R_4qFsiCA&I0o>KOju5% zbcJj6SsTKp6HSrih~IB-A9>l;&UIOgWa?_9=P1R=#$ zHhwjU*oReC2d{Lnw>ThiM)l)s!mLe%@|uiogMCJgPW!#`*z59L_qqk;i%?#-GB|u6 zrd~Q9!zFO1XGX@vJN6nMCv>#>m7TLu*5i}Giax|)`gXuTm2MCu1}ZSi1=QC;MH9{! z_~kbM5KvokQfQIeIUqQwQuJO^T(3O#)8;-ME28-}g#z+sy*qa;Z_!dfT?ngK z=A*!;xEn=V`*JNtVPWMNpQdZ=^TUUsnO2~v`rUbGKcBDi&M6|WXB9y#=&?IPzY5Mj zfMFYtKGoQxh8DU#gd{LG>5@)7&zjEEA=ZYTGsGZWnyW9KL$?~axj9X?e*9+VQn#~n zNQ|yo@P6*~`gdm#I?V*>yxZTakH>6X@i;azq zYY3IixuO5n0<#|~e1;$M2iuO7sy+*9Nc%k=b~r{i0HMuDoq_^iL8_ZQ7Wu5;sDPHV zw4~YhIEO&5qa+?B-QGCr9kNL(ZG%3d?Il7U6!VvRk&s~eg+BTL3~Z3=72D|0d%LXl zZG$BD1!7^AQlB^IX`ktkN{1RB9uuG0=52lbnyA9Ro6%h*pmDXv zjXuE-?$28QCc&}R-ds{&{hOapeL|0Ofuw(M{VY;IwK#{lx#;M%Wl|aSWa3OBHrTEd zoKeZ#3c?hv^hq{ypHH3gkkGbS3r)|m~!A;*#j9q?i{k?A#e zv*sQ*v?lrBxr&=d4<3wFERT@mTo}`LahV1-)k~aH789rUx$K4v>>b=WD0tof*4S>X zeRRC*AbAnbI=??dvFrSi|nJhod{skE@(EZHY4OVnl-~+5B zt~|6eUZCTxq?}Vzlg4W!>xS5jW=2sKHCYnJ=|M2 zom6H1NCP#Gu}eF7fv91MWM9x#&yh3ouum=6%t zTZ}UMkpUT4U;Sz{Xh5TWvXSp^)Yq*cUc4krltU>=5kA@;L<6lC=w!d-=If+Py1Pz_ zhQm@MIz3TzMAa)nHOSox6#&YJYxMl-1W`>VCq+(-!xPR~329m_Sr_L^P6buR0{Age zh)jlJK%Gum>y-fdEJH za<|j7g9rH(Cr&sJ38^l-+Zautpoi%eN& zV%YB%^Smu*tE6wZRt*tzoQ#VrU!MpL4UxL8aBClq0M&&vf(BjYpxB|l{XM3B2ARjF z27SM?yMx^JP)&WTY>*Sa9vL^A`Su}aj{4r5=YB6E$h5tNTNB;FxhDe|$q#q49-Vho zP0ucIktPjL1?Yd;p@2r{)eISsNj^3tLbJx?K`K(K4zb00S8){<4UrqKt1G%~2vR{l z&|a7K=?0TE=~w*&pHnR+WeKW)96U0BO&mjZ&2{0#`Ccr@N4FkJoj+ zd}a7vpa7IF8kdN;iz99Lu9vzos-rg~n^sqlNLCw{}*L6ur0NkZBr zK0<(^ydibliY);M2ZBRGL6;EurgVbho2%U&NRI&Kyo@Z8c-LCcz(Tm;a zZT$!v6v+6pmolJDP$(q#b!t)gfV#pM5`$^@il-m(ErChWSh~k7eKuvKQ8`P z?5PVE?&0?yjK?zrmml+TZro1>EvJ;#KOtb+=`DzY1PW_k+zJj3Zf&RNPrFRxsID^U ziWPRwBSrN6@`xzx>Fg_AhFOfOfdc5xWBSfxaKXBL@&O7XO$XlKZEK);gbw&e7xrrl ze$RN3D;_xxP5|lW^S5zYr}Z_LAu|X$IoVNb`;yItLfh6rU`n1n=st0ex!iE? zEOdFTwNnJaTrE(1kPK8q*S9`Y4K$Rjs{?`=eGs96hPMJxrz_a+uK>Ux2#rM{ULtmC z4tNmCw1?YF5>;2Q?4UyjK20xWSu-kYc(b|2cldmO5+7Ljh{E5>Ba= z&4ThA-P`YBQ;4~-f)byGzBRWuov6mt2WLAHqdgy=*5Wwf&xj<51Dd@3skYOb;v{99 z*F%uCEKI1y|B_yCSh9p%>w&UFJ0i@)NyqnS;&1v2UQPDr-}Xy&1}RwyPfmo6(at(= z^lxM{_7g)n_NEh+_~nY%iJ!dFj~YyF?l;*mBB*W_-M#Q(!z;@w9eEm{MJp>Ol2cL^ zCG_L}rxySsRZZjII#F$DfYd%x)C=xx8J`Wx^e3BkUya zz0K7UY}Z(<##q(J*1Hb=R1jA<3eG(PJ*sCXV^HOka!p#l&pRccy`dxx=8{sFQWsN{|BK{n zj)`B>Fa3Rzp!wG;-=i0f{!&d^U0zc1YnshuY(L%#Z(%+8PBD_f0y!Gf@GteXTeqTj zRd3f+1&?J z2TB|kmJ=mQlo%OG9tDOy^!}!R)dwh(Iy|1%q!vAIj45M|B_ykct-S}K;i1Dw9+jB~ zvt4fF7hPaO$|&BvrCJdb6#!#-a{3pYLyFJN%oH7jV;(El0M+?)CE1*(J)b^n>g@<_ zN&H%J4!FeNkSE1ax?_IX0tUApQB3L7J%<&dZ zrOCbZxx-XaO^t!w`0|m?tN`u+pv1{?;SL4nbZK+ZW)~!4xhRS684Zrw=so061)_GZVc34YB1pfUo#E@GPip;!6?)oM#T zQ6@8;px=ViQ%xu}?D0A~`qB5LxNbvZqksGQ;p7nJ8!-NylmSZtL=Pqg{3wI=i-_49 zL+Y2QN`jhc`OYmy=Z9MQ`pgs$9X8jz)K4as8E|CP75|9oY0p4+UcZ85VrqeGYOI$6>mjICZJBZ2 zWMyMR3djEPH-CShywUaIM&D%+E|~%!`Fg<)lBIaKIo{zO_3-ECnQX=?fLC? zEc@Q`=aI0a_OMh`iiv^Z!yf}+Z;QxSsP|@-oDAUWT&D_sD9fV2Cgtp~46Wy7bW&fD zQOA!1M~-fxY!x+AyZV~E^aa_{?(>t=C4eoW*yGldG-7z6{AV!UDG_<&XyhN7M3>9f z+KxD{?LW%?W@Ck__=9>P*s9)Y8tlO@ezrW8a{+p7lJpq0+>mvuWWxuZVYuS6tW61i4OF_vT7RB6_|2-B z@L~=gS$9U0a`bAA=c(znSP!KPK}%;~>CBzPO%T!BObAqfPqEG86i5wAHGT5raK{t6 zOR$c0BT~TKwIqkRPLX!Ho{ZS?UH}sJtXUIV}yG zrS|rAGyCBi_=0UU3`-`oCnD=aki#W~>bRn&wFc4oNh3Bw?z|k9)^{D5rZW~0 z`pOTC_%^Ee#Wf!wH*=3pt}*`<;Zqx6n5y|-g!pu3esKF5A7DMi^)DZ6Thh)8f8woM zh5K%llP5Wxt$su2Lwxg|(4r9Zthe2RD}kFns^Ke))2SsTh~%{7;AP_hv>8IcGPCR6 z<=V3v_`z=!ZD({uYI>3=$zKSOJt8hpZhPUAW&QUjwj_`udb}$kICr zdYp`P?X9%5p?X%_c;NjSP5G;9IoujOWa02?ex3`zTC2>t}xA>-d@Xsm23r^MIPywUN=f~cW@q*$kGS$Tb4`qHrSHgD`z zJ~+seMS%DQ-p|$5mD!J7~;X)m?T{25IHdy-_B%$@5+;f0ZiXc*!_%V+wwYYAdMy720*^c26bP zdJSxSIf6$@UfyghyvDH4G5eyhFvGNCb?9#~+e-gB(GtLvz^717u*LqB@adj0uuOG@ z86o=cITYAoL~bsh*!QFV{OGyY&F2r;KA8AJOjz@N-;A>Y(x$3NERNpO7-Mk2AVJAz zNY=^eL!5w$6t5>sSIOIcX5A4g0*a!5YfN!=H(o#}=&YgiJ%EF_^!hOG6)6K6uX*gBZ*6CAR1nZA^L3Hz8|0+Fu5MtU*#6W z*%bKnsFPamF!5gwZ0t#+WqYkKu}5j-nk+$VoE z-euk|lc{{p$?G4REp!cM?)*13ikiiXNwM2nxYZ`vxbgL_y4wBM2H=t9ZRB zMAg*nd>T~R$1A3sTNGiP*ExlK<40LtVTDE6vVHacZn)^DDN8M&zTuML5Q;Dq%;Xd1 zv(+6R=k%X5PQc0l69mU@Zeqf%zTQU;feNs|0rBou&nv0WyCm7HsTmU-t-Y;n-`ubp z(K*%5Zk`ozynmb}L8CD<{hGX*9bi=Jd6g&;L$7%}aLu-k%0HsICeGOisOWcgG9A_a zxswM-=0gX_?DKFB()r(h%k31*0*z0m7@?8UgVlytdfGZWEf*HY7Fg>GGt8U%I6Z*ZQJyd1#mab;)v+k{-I$8u_We}1t1I=xRj>p}iG`wu#=CP5LUI*5I6cWLq{ z96!T4%{q4Um${y3h82K~vK;Ml2?WRNwM;POM@0G%q1$bk7&`mQM4@d3_|)*0N1OtW zuz)8tOM~a1pZaNUNy5SE&!4r)l>yNXEOfJXf^MFkV~f#QCq-0H{2~}Tpb}Md!o!>D zgT50MnEe=@qOaqe?M-trnb*_urzRz)7!1r)>cE2XPy;Gh&8@ns$QqJJisS z-dQe}?d?hbV!c{67Oe-!z2{JLGU)0ADjS8|ntS%2J}LT7Oi9w?)!a#4_a0R>mD$K! zlQ>;U)3HC~lL=LRED?@L=O60O}fo<5AFO#3+w zG{%Ipya>&-2J4lINj;4~(0&0If$#;hjAJio^cTg;k5~QmmkyKE%zg3C{CA=rZVN(< z>`yIVB*IGTDdD?o3}{;a;+>h1k>QXbY60f{-rgX&FJTW?d%2Zr?97>o393-b8NeVF zy_j~)dyAA4Xb1df?4f_|=*284K<-Eu(*YvPrmn6jSg*Mb?T32$q{?ZOD6B%7g|xbH zLvevDIv=K!;;?T#AeyPap5iHSwX(wOV0Cx*&JXk0rw0Tz9m8&4)7?M6Q8bK2PPY0K z4QB4`T^2rCeH+M-m;%qzw_5EkS}SwetkG&pp5IAZ>08@duFENHtrl!4lr6Lkk}^ zUI8y{s=gyi)vA7ivKco|FjpCa8N;#pgJ@eA8XPj1pSp~z)3Je|F{?30(fiJ`_IHfSEMnp&Bd|)^81G!Mo=wcb`cM6eRvSR ze^avh38f@4$VmO?;@hjP+;<3$Hw%V(%@1UcXOu2`HYBm;z z5Q1t!`}c8jO15_&qyEOO7xMkNxnK(`xH4P*2REQz}pMDQRoL=E~8OL3_92mA&5i?sYTlyD?W3T3x(mTG`y&6X$#OG8)PYI*KiYT(6$Oe7aF84$;<0+YOd89VUGu<%?E@}itV3931-w>Au&;Np5bs3|RN2fACSU+FxF!P@2-D|Ne{dnO65RJfFY~t$w_Cp6Ys$a{n(|SyL`agJVky)SN@B` zKX9Np{og4i1pZ%mKK}Ygi{Ji9{rJbflZZtB-xuFso}a(|@3Q=(iD-X`{{LJS;~Skb zndk+vq}RGGa=FTgyYtM|u$0P5xr-#vccQ$WY~9CejtQTB=aq#@536_m*s{LwZMgO? z(Pr5#K&FUV5VL<5Hr#N|wG_3XsCrSnHfBeCS2`5(1qlME%uIW+oa<=6*{X++ev|!| zQ~$KxBt^{b_$=G_?`_Sq5sSIZ{wCXwoeZmu%Gc&%pqtz%lbFM613H$>Cz%1}9bFfF ze9kw=V=aB)T@xE&d;``xNOsiEB(Kh(l*suIN3L{)hoFy0nugQwU;Oe5JI_ z@}?hZ;fVc0lB7X|>qC%Pn$c|)ERF941P5ETW(xdlq~PyK5b&S9)Exr)!YyQKG0tARY_u_@a{)B`~jjElql_)SJRRzonHEKpY1OHh8#(Wt3 zq%yRcmdKxsw>Jv$JYmCdf43=z`l8VTjkS|vSzYQ8uPmRq>>sj!?uPxmckLn{B;m`0 zo;F3zvqL_U7)roZF^hzL0?Fb<*Iojcg#m!QYFYwv0Wb&7)IRz{*JIu-W_Pj_l!qbP zEDxNuy)AdKa|Ig6A>%K3L?Hl8OcEUlq(> zeQ^YzU}dhDr5+w`tKgRXcYbg1a@39CH=#-$Q zH>q1Rrso*slrDI}n;xc#N*3$nmeI#hS5rd1kSj$jg(I2@1~vS~!ftze_lF{-7Co-& zjc8*kVVZBxBUF6-0;JRi7T?$4YjhOq4d66KuQNZ^d^daa=}x_8GIES<<5ur$|E8sV z1ZYi|0!=efNg#BUP|B#Ptd#O{NEVZm>k8Q&A0JO{NJrAzRe?BphCX}lRz zylUYBQWN4>&)=6UE@tIqWWXGpKj_5DZg1}$-=t5MGo}lXV~jBG|IWW21>5e9#FO#t z+-7G9qv>cDP-!XJV>t|*MBeus2GtyJO+ZXHUUGy?j&KWf_EkfiM=lrV=+i)i-B*z) zsT#VG2_+#*_?{g!w~;Q$t>l$*BB;XquPs0`l&Zw6R=L7**CjQ1RAecc==gB9wLZ7p zz|@@Ni488C(5nAtZ~vUt8m1$@J=3k}A&k%qp8ut9P*6=%lQO6M)}ZF8`MG=xlw}8# z?5^PG3$}J|HHU5OB&-o;T{HYxU#zU?;W@alLkW079K*$AfDxo|?Z16~?REMn58}n= zX0Z+VEJ)(~UE6mpT{&IdpI&GR2>D4*2Oal&{21%G9z+iP-LWMur&`kR2dE=~Mtxco zj0LLMqhBr?tMW48`rC z77Ik63}VKie!>p6ReCSG{UsisVKCS^x4Ic5-{Be~1P?JSnc?4@VB0v2DQxj^QpfNM zF+0(rzppsf;dcc?E2nG}0%pw-&2C4&2fG>!FDJPArt5bb}ew79Q1{(57Kx zn3%}Zs1s%&`=!zTwr~S;Ok;%n)X4T1fGVJ@iAhaRb1a%YY#fYa?sm?K|M%mhBElQ- zsjSQvqk_4+OqGEXT;}NX&S*+Fd$PQ!D{PF93ngd@VuTuFpy_tv#IN1&oF%Xm8Q7~} zuffyO7!4S9sBc4<-d4*Bw}jw8dH{c$zd@XU=DU1b?DJ`pwi7;eb#=_g(|lRVY55h- zI?1ehz2-Ek$HnZ7Pr2{kc7jg$(`-aishMn-%6(Q$`iK|q`p9**YIKH<0$&;XbA)vf zcF%0b`H(xvh+fCXlYq}kN=-#9EjmVg`z~=z*pS3lAupp9KnEoev zdanZ+qv$A3ek3GOTr8#PQpdLJ3{~Uon~^HF_&8zBGI4`+@9hKwTvz(y#czwXV2kGa5MtBO zciQ}n^!(t{zM4roHrbI!LVr7ZRuljfU=8SF1gDack`tVqGAl!N31AJ{#>POGQq#)P zMRt7S>jt0pm9UbFh10YsGv@p2c%>?qJR5fiC>(*w_J}oZVmJ zQsK%i)s+JCmX;xM(%1&iKylVTev3;8=EK(hwdRXH%`5xU?59WHOm z4qdsAApK@4Zw+8Z)u2T%Hw91jZuEWI5#+$HRE+S>BEzWsRsK_RSF+SL5gQ9?#qT%J zF`4otPhx3sj$Q(|u*~JEcO$Fq|ECuqZtspNbuIn%g9_`lUYJk&$cTZq&xL{}^?FtH zz#c0nS~Mr&PJ&@krYZfmS!Aus#LQxmTh5c>&*7Ay-6F=`iSgKog>YI>awP1R^Vd!q z&e>Nb4xfyOxOPotve;6={k0 zGJ^=n>o;%u<-4wb2&9vTVu99ll4J1nIj`Z`o}v*F$tqFYpb~YW>fWbAS=WPmy@hlE zJ|+~lbRz@z{H>w2^?NL!2*2Cf+RAnKdO{K?Rs(i%*y38o!=~V9+6`Sz=)1oiZ|Y1bJ)+QE)#RvY9sIC^CKJzZk+-y4UTi!)mZ44AKk!(2h!)XUbZ>+w`)@ zmOCe=q=Yli$uQkxd1`(L@$}Xc@p+BsFUQ z$HjMLEu?XSC;%~?$SZho*j?W$3{BQKqH8GFA5e|#W-$mpJX&_u?(ZB!>54QK5p5@}uI>O1 z$G*>pZ;l|W9D{F@?EPY~T|-rF>)%A;FJ8G~a^uDgzUxoQ!EwiWkEn`?i&N9wvU`A7 zpMrb$@3i;8Vkt+2Ujvm&7>^dpv9Vq!PM`#c&)8CQ-dxCYZiy=a&Yx>Gc%@YWn4CDo zbnlqGV59E9$6ZYWYNYp{12sfMo^~`jwxgqC0N70dphJB5;zcnalPBYYFo0La zwJTc&^h|RKYcvdZ&bDcs-TD#9{U0+?PHZihntBMJc}sAs)NhW4dNJ{p5iTY@to<;k zxOj&!4D{;c8w$dhEdKM3k!+JBf=0#aq z!!zP{yWMi&;()wdk}zR;V{4&DS6HTIDGf!EsKarWU5uao5z1(E^V7_{9&8G<9l+2R z+P2y~55HKCdcnJ*t%(7=V_+v`A=4q6XN$kFaF@DLUr)5n7Kt`4f8{4WBK~zs1Ta_t zqVH?az9IwWTHj6tuZWbC9uOPD)uq}C&QezkwLBs;9UKr-_f@<;KJN*4>bhqBDndxMW*cK}6?T9gBiHm6 zkgpK$3WqI$6mL)VQ;iozDOOn15##|Uxc1a|!>Ij|>N`u#z(kEBk8&#b&DtN`?mz1rOB1^Wh#g-)ls`YYYDOaP*o`U#^EY&g*2%z3c4+NC*w5Aul>R3jEK zUZ0aL&3gg-%-k!9?6-u7y~weLSLaI**`{E_oA#J6*yT^aC4p}m1Fdj!jSq%b-qo7$ zEu7J~Nqx!V*v+lElUp+yq7P)OaF)x#JIgJgS7W|k@yWU6-0+E45}u=}XP@w@JT3r- zQXzQdAyEIZrU0FWbw&i+4yf|=eNU4Q=ujjL!g;|vw}Lm{KQQI@n?)f&4mJIHaAgx~ zQOzJn!{O8^P%5Z!PV3sYXB=>O;HBoUB*#Pofgo#No9jFI6?kcbeFv==vgS@>l%ZH) z&f62Ye)h|~%YzobqUPYmPJ|}K_wTZ^7yT7$*t?8~gq3E2)tQc}x-lRcfdns6V)8); zST|~Rb#1L5++~WOS}P_d<}vd#xQDJKxVq!OGJ>udK|Krzs$l=5c&-1Bm`6itaxRoVivpZB z(v)l;(V4o=)SCqF7&4a^t+!j31~R(cOj&_k{5sK25!`NQCxYYQV_f?m%Byphs@%Fh ztN*NbCp^U^>xdq!?)ZPl(a3Djayqw>r@cVtx#UHDdzO zInUPQ`R9ILk}@kay$A*e&~ZkBhujToCl0$GaasaFqlv3)Q3nrQEpVY2&uFp(51NF> zpCj6CS0#GGFfcO!%wKNWVp>2(KmUrVi@v+n9!cY|&$O8a{j&vf% z$>)q7(1@x&>Hu78(eX3l-wF%Bl1y{~;Q4zWvn;6!+GLF8Z6BL(xSyN0QpdB z^#D? zfQZt|_6gq)n~_++wwFB#uLnZ|supwASZM^wXbY~xx?lb0p_h2nhCnzIRn%g* zNxqFvdi+7R4(>;XB^JK=WGEqILM_>AVm>&#!qWnX4&zu$&*|S2#|k%uP(!yRNTmT0uq8J~fqs!k-H=-!PQ$y~M-=Tp|J1a`=gNf(UmV(A zzdi_#6*Y7z;9!)mw;ouC4{K*>B>o&c64cHOrG*gD#w`2miTdS_Pr&R1yP>5{xE+0W z2fe(HA3`ua%mIA#q}<%6(p6;Mm`8z(83tL?2?V;P)r=98g9H3Wqt7Y?44Wl?F^k^| z^9_2}MgwU1-=|&Oxel|7TE7?je*c`T?C{nWyyLxn1q;caA70)X7jzl>vOkNazbb#y z>f8J6K$cIBMQpylJ_Z0%@y)ug0m-FBV6HbmK7E_-sblrhEACm?d)+s-FSp1?Qn&aX zaWzj!NqYVI@n|uqr$;wv0`LK+&&+woa|codm^BG>J6-wp+ls^G1@cHi;*M#gN!2o) zS=mNG0>qL$tdMC*8cJZkV)=gDXnV6U@4uItY-y&iyw*EHMwH#N8q$UXY7yM_14Tx@ zYaH*n_irE&FP_^XgCgG9w&IVX(;*s%4{w5TF{$e{@y)|ldA#+oVPc$NoQX@Wzm}+? z<2U~C5pC3;c7Hy(R<3G*gWzxjsnQi&>XvV~lBR~@Yiqq-pw)dTG;u*;JM*(GTW9ug zpB4-GL2*7-?oYDLlgt|_R!8I4c}oQScgdx$e~_*K3G*grN-LENQB~@`&}1*OYxtD{g7)>qk>@7#e~Q z;Gb`ZYtEsb)V1f!OI`oW!nj`eJuJU}_y`aVMb3se*puFbm?#(&`t{8TcOX4RflOLj zT6jQa9BV@V^88P8*wa}XzOb|(9%X(R$|*Ov6=FM72HA@?I+Ie(J_>hPTu1R+epkZg z`HvzLB#wNsg?OVapiVtlq{4hreK4yUoS|9B3$Bzdkd2&LFA?ec&kg6@Buon!rTZHU zv^dR_&W23wtOxe4Gl$?!I9dIZCI6cVGfczluj5|a^5lTzu$cl(Uo|1ZWTux3p)2L@_&d^2$~(Wh+Zo347Q(Iu6ptKB zrvZYm(I+TQKR;Aamr>W9xzA3dKZ(*>Fw~x7JwOpr|m^KuZGjrGJ{!v9yhinjd2XVew@STG*!O*SY z;kB8OsvuM|3Zj>eoD%Bm7fv%ajlT%yRG%%R`RW&X>*Zz7^LTrHl25lg*=Hno!Ob%F zi(g&dV%l?QYswq0W{ZX1&y+lDtGA^^9RDI`b;34;v!EF|ncP~xxzEN}5YD`jNx%fJ zi|Q*NKHYa-2I$cK3j;etqm08YWWIf5ZA&cACaypzj+6Q1CZUxM*3w>`1=rA zwEL#*HxUqTrUsf!S6Y}nnBmrJdh-n4zJ7hbJxNYzKq`?u=Wo>6&Wea6yXv49Dkhj1 z>jEtrxa$`$e1ca4ZrmC z$F{OpNrPo}1u_mbodu&~>1l92_BnTsbKTZq$G5AP4%uUV+wy_)q0MO8+*)jE>c1l;Xa!kN^t0g&}KKu zSr81f5mREPrWTPV?Yr|g)dT!9#r}Md0$=9yu!lnSscre(&MT3t?qoDi+ojTC)b>#3 z25ZRkW~H?Xo?~79gdJ7a`U4#riP9OLKHWf(PlJL9wB+)oOG*G-yMgydhKGL?FeAXf>}iy zC`OAP>?fb~SZV?p^&uRMe&k8;oa<46EN;lhR#b6f!d zgF)m$7@M4)MhH%aH$#aiVrJPnd)01vvWbDNGc}V7Kxbe;S6DacwHxC{fShcg6Z_5- zjFhwN_&G;mlH*~{zAsElWT+dq#M~=lVtTNZX~SaH!KioD>Z(C zdA4<61&|=Wify-uD)XpkP~BpM!w4tJKM`7{NpTytMXm~RHgVinGX2@Jp6MX5CXPox zi-lUJ`#|snQdtX(lR2A5hJmM*nsrBu?C;>7&7A+k8XBA2DlqglbGlJZdrHhS&%xS0 zl;gUpW6ks5XztZMB5baHhW=|po)M_2u9lD=LeJIgoLwIxKg_TZS4TDI4ogzaBLX# zEx-n3{74sU3E>QEGHfh2aA@J<~2uXmiq?^N`fYd4t8VOgWo19rA%cWH}#uI+MJpdFlK zZ*%#_Cn}i#{7s-W=@pW)`%_`V_a&AbBOtT7HRPHD5Vyb0&Ww1iDG)R5C*@OIO{&RXENLCs zn){|2Gfm5?Oyy6pAE`C~sN*x8=*f_j*&QlAv%P~*+dc%+--0rf**f~B_qTn2Sc9m{ z5dJ`SeJsjG1j?oIH`9YYi3%jLT@-uxX_UDF_SVawNI5$@Gc;iUS84Cx|E1CeB1`Gx zCbUb9Jj_g0j}?K_a$zw*AQ^SFkt@&`;^M%>496HcKsG+f&W-`Hk%J@zpwpqSkVD1<~a?tb)cJp%Nq3 zt^`&lBunjR%Oj{TFs(Y4dv?GoSr5c`?pZ*Gy%7Ih0I(t#AhCG+Aw*OoRqw03?y76y zWzatQ7*cbY#;bjBpRLC znWl_^w*~q?;IGz&{@AnU&sol#$${z_AE;GFbY`Kr82kqZsVIGSiL38feGA{;xYBTy zXbYool~6-4A=6Fg&`0c3#JX;qG!p>=9!C-N3d(r~WrrYf6y!&_@Jwywi=Wi#svOh%);oT_{twetr|VFa!w$;n;; zH5Q`w5p~ZOXV1cu76vn-Xc?{(3isXah72FwRB!}$F6&TD_H?-%*0nN&Uv=eH{Pf8w zl+lext(}g|_WAVR5pH{0+uJqG%*>!?*gIV5D(1OhOawh2j;~6)OuojAg3g7>&b}P) z>`2%tV*szn$dhtAtKA;1K`&ldgE+OkvQkn8uGP>p*OeGAFd7XLWoGUJO4Q)hNMML} zpqAtV;=GG4E-7gtv-z$slfcI6$c zGDd@^W^*cjd1)!U>2Wb2f{QNTijeCJPn(tO5vZ*_Mz45>?gTofuaO2<}9u z!)9II&@dQkPpAJRd3xzuj5I9CM2yJ%e(TT+wu3yYsZaJ-71XR3qYO;?+}BM=CM1$4 z(cPa|ucvjj|9;`P_MP-YUHj7y+&c2~hL5JDNUS>_!;9J9mWA-T;NZjPD7{305v5 z?Xgd%zslaHP`ufU=G~DFQ2%7-qKi&pO*Wds6LVUZIm$6SI`N}tCcMe8YN>HN4%9j6LL5{w?GB;F??Z&64 zbNd6N$1gYYb3_A-GD-GZe&bNPX$iIT3cS~#@!bdN1)W7VDiJXcG=H%W&Llxc#hCHN z9CI2hG<#mVImJ#PRUyE4!;lt3lCv;;NFZH4fVYXzc7}00XUyW^s~F!4tjaB(6I(g` zG2l(t)GH-N`erUu7g)`tUfi}N3OYU*7lZ8?&H)htvRkc+@$E|h{@JBQ zF3-&X$EGD9|7LddX$AU#Sq*F<$lYaDxMG-Ae~&@QbgEj3aechFpRx&(&Z$kwYKkvc z4va*HJQwg+5XDirc7GiJFPVYNh#?L{mwQrIf)fSLj1UH0ZC@+91OH zPh?Z+qPpr@YKb@$LQz>+ z!eFbTr9}-QIKaDw=1v>Gualdb3&9OHRkRmJYE1rs8P)@K@JDDknch9=so78QfTKXU zg@yOgJz1JuVOxB9w^_@96`k9oa_6S*ffcC9EXR(0bO3mjs~xJRr^hH%8cDJ_KYl!< z!dBY3=aVv*e|n6mj|!|WrI$(I$K#N6y0JrBcNB>v4o`^n#I1B_XN_+s&aHG97K|(k zcq85QlWJ?<$ubL^IWTk^MwG&_r)_ZZFD{PSG?YhIJIXxrvpJ-RjYk+ezkJ6blL>uH z6v(?8^AwLN&VUUwhJ?VVtepOaP#G<{Qct%RL0wQ`hw;wv zGtt~T)5yT6zyqx@4vy=21Wa003X<99#spr&sj{F;KgZ9tpey+LV?-2wzbC@)K66+g zvumrAkKe?ij5<$gpE9Ad)5NJMD^13UZ~q^E*#7wRv~@)2+__KL{S#Ll9UZBQqh7I1 zukKya!R!?=g{13|nkp?yzSJXga}#~H`9DDb16|Nj*WdQdRCR4HCsu6wV}?dOqZoX| z8EyqAn3PUY6LVKO(9VT&VBK+Rey^|Ac3U|_NMNkVy#!_uLW??rTTdUqTc}UJjeMwn zWh{E(AD+%0j_Eu4W!z0wBMD!=y>^NresWn^)-)ja#l@n2QRT!sBL#*$*usYJlCP4J zv9O=V;WVe-n{YH#)og zA4#yEV(pujiG#@=cgdC|YZ(wdLRFuk6sp5o8QFT6w0C=Fa&bEA%lL5)c|(8m?IgK4 z#J3*_CA>2UB_@b>w*2cP=d^{Yi3V}|mDT|+6m6k~fi+;x2@L<$H8njCddr{p*BwlZ zdK~Drhoq-}zB&AB-P3S)`Ffy7r6!E)HI3UW@Vj@3G5G~dC}3eY5Oy|-8z#3W6%pH| zBj}b_yQUJj+ylFI=GfzAmIAz^W&IPxmBRy$;d4d>1k2CA=oMvnz*BB-PjJye6p~BN z%eyK^`&r^p?h7r@vwXWaH-ZkuZT3I6>AwdObr;AnwLqNB_%GRO=RO_G5)%#h{>@|F z$m;m9V}d0E7fJ?FA*L+IeL6v`sGj0MIiALjt<5u_!M4CR@gQ@E3L{NzoC7;#V_A5q zF#?(xtNY9*rxi?^gzH!qPF!+~2+RGc2d;g6izr3K-bds}D;wvANA_(b+n-2_^4Z?q z@=Ck-zYHf1ri_Jj69NNG{FdtBEYZSD!FHZAr}TlgebObh{xn-?`w!w!G2#xx*`T6l zr>Ecf?YG}RRnh?o-EF`k=E|i@nd)mYM2(n$fA^d|Svz*(OwRVRTQ6Vrwvstyd@3LW zheriQbH<6-jzdXGdT_2rEPAY`}oDBkTW?@|8=@InZ#}E-un5Uy(skY z{Aad!k$Vs@nN=zTz!*PQd_+&zc)U>d^0gaj%G?+u;~1SmuDi-DBBCg|OxG^nqSjQt zxT%28H)?1n`Ye)9=3q(Thi$r%&D&T-7`*yOnmVm~K~&2zw! z@V>%U4qs+9)2^?P+~(dp_H6QNla*B#p#tk}a{n%C!hg-k_S%i`sIVo;|*j%;zRw$E{gU&q;(totf{{CD^mfBY1%=NWa zkE&XuhY~!mxD&o`x(EDYZ0)w)RruL~oJG8eW08TTnWMDAKORL0(65y#GA_{>bv8^g zEJVQlkxo%gjL}r~{2#vx{DB5;Q6ukuM!t6W(TdvvBLRbPHF`n|0Xb<^#M!_8DJJ(< zNhu#HRNo?hcE-WYHuK%WKYeP7>;H0e>wn3A=K06pWKYaL+4cAAG5-I_hr6AtB96f3 z6a0@yOWe+31_uB4|NB=JQ4pnQq3l_gufcj_1x7Y-8}P|E+Wy1A&pn1M4!9O=yKusl zl(edG{j4J?Fu_Cxt*Y80Q}G`~?C+=DvG!BHicgJSPwRnTe7@?^d6G0<&0*8c?Eu^1 zLu&{XgGwfg1YNHOg?_etssD4`d%?_tlC-Kl32V)+EzpebW}y5LwL+2@u9&^E5)>+k7%|0@P7nl?djM&+j3=LMsUxKh*yd{^V{;Q zm%j+5{(U$8OkuDg&2~wPX_nmd(pxN+!FhUR|I0PKC0w}y_am~)J@V4}hpwDDEA%8z z!admp#%3(%=CDqYd9dIDebrKbS5>K372X$}Avj3e4h_8i(ZzhOlGOh<1{| z%Hu@d`Tu$M_WZbKF*oxMN6Po?k>>q>@6kUjlu!S!ys-V_ZApgeoldD*%T0xBx4D>p zBdud&6rXBpe4w`COE`-2sQ;njiD}xxgSh%FBF>(>`Bm^W^YeRyFl&X z3J~B*o=RkkZ@pASTIeiG4ye2jDL61mR>)>~LyMEr*Z&Ec536I<*vBOUJv`z{<)-u_ zJZrOLmmw*b>ratczl&C4Vo&vTB#fl}eXEk|4u~H z^2*BN=;f6opqLgA6qc;h&X@WhV45ApwnuZgJz8ll;rK@PPKZV(7lURv zs^efM*9^EZOJub=&5g^rAm!Ad0yb=ddOQw{9>aZWk4Mw_=wXxG+*_cLy6SeZZd zB>@gpw2<*Im$1pQLK~M|sS=R<=JYk2c%h zgjp>A}p@r2YeR7M!GgQ@d3+#fRDWn!-F%Syt%n- z^biMe1Y?FYjrnYEK@E)&;2lKGu?dPa(jix%`Wi5G|989i6brZd+vwFrP+`uZT^Npf=8;^5HZDj4N)xgeNU zsh8YJZ}`9*EqblhAO2N6dvyV6l*usCfCq;)>Z2ZeeXy)t+trz)LRM;q_NJjQzaRldj^;FP@#>WB9$#|q-he+19qJiS{Fn1 z7^*>n8)_w*A}}%v-wV8Cj_pML#)c&Ib(oG{dBNQA6QtY#b+(w)4F2BBGQ0?`wnS{; z_NH*qTif7T6SK2{bLmC-(*J@;65iQ~$0uzJ9f2N3XK0^s?$ezu@v6{gsgGD#W3sLM zB*NB(;{|-CUiQI5<@;z=9YBvqiqk59L$Y^e{q{c^%XDtErA9KO6Fv1{4I;%tj9%u>TQG>Fm`E(4_} zZiN~V3CL#+A!mEX14^@oxA!J~Z`Shi%6^oV3jOY(OO)v77=9TUgaw)9ZOB1H%!kWR z8YF7WP+6GClov;^aw|%R(?eIiU7!=o|eH2NoM&EguPv zjE~3i#jAH}g{uctNRqaCPfZuq24`vNsJxlylT+9&)oHx0sfym>y<~l#ekW$A^2CV~ z1LFZIM6%s=O-oF6#T^s#R1gDu11)vyC?lkT z(F!uJigrx@X`a;($Ksk7rUm7(l7fndYJ9wUvWWBIIVpvAVUV%3;~aO`+U!W{&a_Q1 zF?X@gA$I}U?p5!Jkx#~XhDW47-HE4%9XOgO=N$4@O_glF3}fF^z|tt<9Y1Nl%$R=r zZ1294Fgy-?5&QRV@?Sf50|@+EC@Egnuj|~?QorQw1TkUM?zv&Gyr7cdDX;jcfpl-R zmx}dIiRw+sA3*?)R&Do*3(q(N!=?|-r|twSE!@OHy`lXeyXbu^CTfbejr zeV5MFJ+}HMhrEmKnMTdi5tV`TZ~mNg+Qt`01RKr7d3G~D@H{A1BI5DtvAiXE1_n6` zq>0Jt?f~HrIQBB@y09VH5qZgP&I@(`3;ftuu_=Ywo#Lu3E!06HE>$$7w!4v{zzg@& zlsLc~-~0PJIT1>qMFc-C zdvG?Fzv8+@*J<@wQ6*TsYYPe~xk`NZzsK#oS@PQG_{=7oojhcpcHs-L>W#`5HAJ4X zm#NXx!^K+so;Uzp7wYQR?6U;?mdUs5M~Y1h45aYea4^D;_*K8Qi5 z2uWL(fvc6mZ;a6a>%VFkqOmfqw|p*0eGnp%>td!1VUwE6ZOP=9vUylC6qTNrAnU(8 z&^I`A?)O>1O*vMfeJrfW)Ax(&Sil$LjTG$KY8^@tY+#_QZm}4g*V*^!i~74M^=b$y zlMvtanSyz2ZBH6Owjbw7nDVc@J0Mlb1OZ*Q+{qcN)<3hO_XkU>jSh~>;>Qc{kM%W@ zPW9?K9y)rc_xbwp1qFra8%?kc%^hdYFDMdjl`rEzh0$Z!os#@*8iKwWg2~)U{dAg| z(1*+)N?%L>C6$cCW9(%-)X=-XU^|fZD7Q4W4qhtl4O5_W#Zlvl<(fi4dkl53R-YwW zaMrWUmxi#^{@d%9i!@jGj^n0J69<M3T^qL}2BhNF$rm_e6Bd3MG(Hx|(Y&Qe z3YgFnIGl#7_v+Be0Ox{}^fHMmpi8dhtB(+6IFpYEL>E)xsv^faW_{Q-LG zTvH>bb*B|8D4wb4FNi&3gALJz8k`vW?j)HW=4{CK$k_YDc! zhyN}Ld-ga<8?I9LREVy}#;e>rjkvz+czeI(Yv$M0H;aa09s_pa1CMN0N2_>jTDQ35 z;<9iuV(O#$dGU3dQ&q{L#kjO5Hv)GiG{d~B<@S{7!{3FK*FEV0)k*bC(xeO1bZk0c zts&10eZoli?IAH;cRb%!!A=@i;77R}8>W^jk2gd}1105N5H2_?>!QomMpMJ(CKvFDV6< zeZ=|svuAsG^sJ;DDz-#gH{ELASta#+;o*0ieK{@pGuioS_gE#kPwnoN{=t1IJMa+~ z_2-zk9&xPcIZ;5c*Q-{DQh18^J%6`#pQp5)jQNx=pvyRq-rtXEaz4Hq7WzhR5y33#xUm)L|lGiaNFMw6giLdmkZ%wlrpy!{*o6vCG+fsty6QmbKR# zC*<=D2nMKLM~^Cbn~ptlZi@mAeOA*`HjN9q6Px{!V@h`)dDc7~0mw-1!|V$LkaInn z4lAapx)#8?yTS9%MOO|mWiY@R8UQQ);Wt3NtMK$B)w{5x<(dS;bjXgKfpz3$AeUKP zeXG6wgN3dSYOZDU^F?Jnh}iz|<6Mx?Q{^xVwz2C?!cj)3r7pd#MTwT-vc$v)Oy9`X z{fWlRnX*gGI)?fB_MfoyQU;z>S|Cj*xuIKT_RLF^_O3^o(;}+_N|z7Y*ZR@#5-;1uP61|IWeaG%F1&l zg>yq?=H*OTRx@u#M@CcM31|5W|c-FH1^7y#4n1u)Rk@o8W3% zu!Q;hipBDvZ*X!93+h>YRU3yPc{J@{}sX{TYryz#pGPcJI#R$8Ox-P{0*0>y2I)vCsqdjAe2 zN-dLeYU;WJnOh~cJ1f@+G2Lv>TjE+e7j%^9&vevz zGuC2(^+;4e-j*kL02VS2(0weTxpN$`Nppi@`mXYc5aHS&pdb}`41Km493UqD(#vQ& zX>%a#^of>sz$TsH#|De-VFLmqucFcm{*Ru_Jj|TgH}{=jnfhE^I>B2UVtWNQtjFt| zAvLu3hS4|vt33x?39HsaoO0tmX-1$~WSb{`ZxRdO3=bb;>&I#ySkHEqa*{N;ZkkMS z(HL*avy)rzx@Qtd%!t$0kKpz#($}aQwzI`KF*W49xxYdDXq{EQS@{rkY#8fS zaS7=RZC4Z|2k}Y7gGDPZb^OsLZ4=Ja0qM2r?22L&bIE=-r)M%B(V_F4#>*!!uOT;4 zbwyg_3q|&uc`PrL{44{fED;^JM!v8=NVok>?Iyk@^vmrG3o<%4kaOR0jzEPP)K+AF zc}zDR4sb5^{gg@;qaV<@2&AaJMzq^z6K?!!vczZ%w}~g2r!`3^ELvt_uk{(lGl!{1 zbi8iR5JSt6L|Y;+5~hG3Rgk_)_*H!+PI|!k6MJkzdX+<^#-g8O`-3w3MhA+5*8}De z*WgB;D9!yKwVV{G{83%Pt+HRSULz_D`h+gFb(STS#v>2*OL)qEFiI3(qb_x3$K4-g zw2(_SgG25Ape|y5+u7oSX9I$*b>N_xfT^KLfHYjKW8gK-^-=$l+f_iZp-7Ndeb`Ez zU8#?Ug)h$M%5o@#XRyw_Y&Z7a$qy?v*K|*5hch>mzs{@Po5=u*(V}gT!)Ts9{mfvm zo?%`9v>NPvW!8R1v|8!_x~{*YSh_*It6e#eS3a8DxB4oSQ8BhE&nTVggc1~bM(W-h ze;$s^68u?_XC8f&r^JTJs+=xCMY;P zu%uW{o|=tiMfN2Jtfz#FtbdI;QY*IC6rf0qiCY19i1Q(|05bmcAoT|gHlJX+w^5ja zAGU>0UuG>`x_X@<5k(%mO{Um)E!5Z;7XS2B>8b|c3-ZbYvwmWRqrEd{{QOQB;o36< zt7h_4nNNS=`F+%hUJeHPJaXM~R+He{k>*s$e}E}#J_9i--0(uMNSICD?furJRoy#Y zAKqD5+KiRAL?t6HL%CwqN=QOs&tfj%`X2B2!cnfX%lV@~L-!VBF1cNnyN!+%xtddt zwQ5dxo}0Vy0fF>nCcG$HKTV8V%BcB2Sg5D;_LgVO4ew-AIZ?|VC+g2mb>0#eoc}Z@ zqrL+Z7^N3>PV6x>3OT^xZ~uD!IEHhH6bTGGIdCWR(mQpVr3z^R;$=n^-(H3);*Uy zS9$p1kezt>=bthTwT8gjWdtY8=0L!u>jF`@j|P?R7h zrFC-^HL*mIFeNW<(3m-Um%c}6v@mnX^Ql1mTjzcgb}n;v9SfAnlYdD>(qP3AZ-O~E zQ$052$EO>xGWB07@<1RiLrR525AyV4Rura>A&$t6SIr_kN$J_Ie_3)lf<&N1KBl6| z)`Nt0ssvtB%lS>M;YR*aLerpqokMv`*N;DjOif*KA3rWKw7|yC3q^1vN4tI5%88ab zdU`9C$e3s3U7~Uwlg5OScHCedsGkHI@x@Hx$!B7iEPWJAo z<6L!41fXXdPEF&0$z2(ilc#?a{;WrgieEBAdX1b6&68L{qNn_E-?FniKGcu;FJ3bf z6XovDt}V1~Ivlpurw_Q`v?p_{Xh39j4MriPH<~CKdk;U6i4uix3)(q;!pCO#!!e(& z*cb-J0Q>}M_8E)Q0;hs^sm)hG*jTF&@~9$Z{~_$)Mtw1U!a!*mL&a}@VKbR$-$DNKp*RC zVEsHIHxVV`m%S_JS_ai#`=(iV^i^aUzmVK$+Ab(B*0XQtT&AINY0{UDLAhm{4`Sj> z;c3xF?WSe`%-wPt@sr!^-^XMR=BpaIGl|5tP)%kFN-+l)h$^SZeWCB!1;)$3mqi0s zUtekrY3nI3lMG9O7kz7-Fv{l1&L(+IE9PGEoCi(#V$c-@!d;}mwm#^zal)=;oHVMO z2yu8RMGAjM`HJM_`}I5-3~LE~Yx&68FZ*kJ8&KObV?GOH2}U7-|2bG=m8?Xh1*e87 z(6L&I-*p5ySPf;8#3AmZU~Xq?P5)u5doB;ws_TW8y+=&M8fTi}_1aR;Q8wF!4+@Jd zFB>M7(v)X<*$YGil25?HAMX!0By94w?u--gJDC`{1!X8-iDQFKBatxiZ2CHo?EIn9 z*4aOPx5A_2s#5N1Tfd(EXs^kT>H(?_+HCSW)?$IS%*A|2L5_OHcW;6EBe7{=lFL-= zZz}Xe=CPBBSTQK(;+vZV!XOC=#?WZA+2Y_fA}ie_b-3Eke#5z{A*|p??*0P@;CfEx zlPVOA$|$YjzdMtAn~9d|Niyj#S5b1 z79U<+I_fmo2U8QU?XB;1{ihQr&+khtlOoYm|7H54Yd##SpkV|(a&dG;CP|m`p6DQxA_<$!lw@k1`Glqw%)S8VPz#t zy*DaG!?Cw$%uvPPB~xay(}O88Im7Jv@w3qsx%cO*S{Dkh7I{LhS7nlnlk_*)*sSVj zt9s20jYZBVD>cISPECM&AeGgWd6jvFeR)=$Qe z5phNy>Y;DmxOkb;#o+#3k-d74A@8xX=Ytw-KTyVS%*pYs7COzUW-Q30lw5k(8Hzw0 zn8bfon*ZXObr=2iZ7{CPvdsbBzIxB#h^EPmnQwP@#%2TAydw;e+w1X4VXT~|a;=hm zhRp_EkE!@%(kC3&oE)1xM^Vt4;8tmuC2s7tHm#*l+diCm%A#MgTe-}Yn_HZn*PI$w zR*O~8pp^_!e5o@93{AV7yr0LWZ)8V<{3a_&>@zuN<(13I5z)oqmx8=@nu)R+eY@k2 zCN@t5`rcHm=Rv2-IMy1bDRJ{#^#3L2LQjkXM<0{p2t!4IVY1XgHblc z?Z>~J+F4y>stI4yuDq{)2TWXKWtmYvMlNdqa-opV2fKD9qWqJ^w=Z zia>xI1(4lk-`4!MY`cOu4?mmsT4LX@a3Cn5&9dPA8pM^#xllE|^6n9b@e| zml{kn@+droS!#LW;)9EN$oXlZTtQ_{w&Qt>_h@Vo$v(_r69oDuYnZ8;e|H-j7A`sm zAX9Q+%>#2KCOBU2I<~13LfEt6F<=WM4PFIkv(B$z3Bp_MaJwqx8lqV+QckX zj43>;*vhjJ?|b>M?$&de$px|ht6Ft*!ebms*eP1tPF=|?9V%ih%ef&gHu{LYNUR%x zNGhpW4DLG+=o*F=iX^#5IG5Ofgt=c$f2JzR-IYtQ@<)P_V^^q7qXV!17tNG+^|n#v zF)_+3Jx{NbYWD@nj;!6quNXH)Gd8|m>f3#HwauK1gqeklz_0ptaiJ9XB%VE7f?R#& z1k0$L#^;;AG1~2n7R~M3##0Bu$sCENKY^Nrx-Tct=Sh7Gd3xd^;na72nEc=1^>dz* z|4^d-n$uk3vEM}FfBN{7{vMq@-Zbo!5qwA!i7k#9qJopv6X!0&|G$H_a}C3Su4251M5C> z%mjy0yC0zSvQg$guKsz^Z!8B`Qr1ILe_Td7isVb9GG(x9CVUs!Ykf%0*UMNgw`jMA zX%3=^5qj+w@b*T!h%e_@_N9hIoG$UOPL^6$`pZz78KnzSCnV&p!Mlim$ zH?>a(nw^yOY1ll@kAt^LDZn&KzqN&H)D^>pqff2!Q&-C5-0>@gKDN<-xqe!@O zYa)%Hed7jIF~Ghi_iqk2v}bff_~L47@nRI{SscD+`9(QooVyNct%BtK$%l1%>rq;o zTki{D!y1PO_(YGN8br`{c^JHUt1m=?p&OtID!v^=Fc5?RlA~a0eZ9MWx@tH059rIW z7gU1hJMaQbFY~DhmySs%Qw7%~kiL~I;@pfv))Q?n!>xz7X>;{SZ!Dwr)p-K?^sCM6 zi|l&gkIYlWD0$9cK!{&nJ`x#&Oo1L8d6JAtuPE)D69??j5x46v=DmG6EDXsnBK!U0 zqfq{L6FMe0`bV*N7h|sn*#Y!lHHPIjsE9@srJ^HlskC;yOJo8ct*!I2ga?}?SLzWm zm>8ESF%o-`i;GLnX;Gj-j3Ceuvo>esFK}jMKF}1u@vMRF($)Pdwnu#8rd6f5m#;5s zyh4kiK|>Rj@8Zgxc)bM~j>w)Wel=geW5#832ZG)|WbD(_FZ?BjJ!{iQA>rm+iBgXV z{fl1ytwMsfT4LWD;A7awzJXIW-?V&SEbhz+mYgMX9o_fHl%ypRw;r+^8CbRDif0`A zV;&*j7wAm9s2rA4kuG?7gR(3Z7tl5-?k^YGBpaInJ*B+2ar4Ez;1qZ76X?rDj%H&2 zW$I^#X{~W#&%qap%8Gm<#ojEfadoyBI6XapPG2D5=brjHXzAXm$kbrN7%a9EYfEqQ zUB24oD%9Gue>y3@f9A5kbYQsfm0@Ugdk(wdgp+@}F%4&^KL+m7)c0@EW;+iodU%&S zOzfjY+Ln|O-lW1rGhmCg z8vgL@o0Ixk8Ew!?!F>G@` za@v~miYv5HA#e2OUpFw+AO7XGH&W-NB5-Bfr8xH~!R`h_QJ^=p-bu{fRF*zWDBD}lOqZrs5P$_89$x$;5m zf>cx5asL`$sVu`R4NfdZFn+}P!S|@b_YdWEzxf*$+u=@KHCXs{glkl;EvM$mlRcX> z+L;_3#N2@^z^AL(W3RH0Uns&qY&v}LM(=x&N0{g5|4558yA3`R%9vA4&vJ$EMWddE z-BNs{+hP*&gqJ#uTG=F1wj79t>dE2*X(}s#P~*$=V@l6T_*8ITxo}mNzA}R?<)z*T z-peOQ^ZixWsf>uq<+j;rnETc6A}(1)%`}X=b;s`Mv98MXwRtk)#Bm!NmGF5u=kmY{o0*=2UWMbA00Jc9ZL$;Ec+NhLE1VU_t~pqbJ&=vX4gv z+yLxD>bB#?>ZfJ2F@1VLW!mdpBJy^2(CORUBz}y4Ia+Y-(pxL?BpY&4`A>D*)oDo) zDHUK^P_S}n94EGM1PsI0MY=v>&wkA8qw3EgqXSHwTHqiWQuR*EXJ|ljOJDdMr9!em z_w{k_J-|tshX!1B_FebT&|Wyb!4QMMQ-#r7}(6kA0EEAYqadcI9S6cD1cgb!H0%wsUdb#R55zcUe(Vtb$n4h z2q#*w02Eqaf(_TubA2o7=r@b#D&^OZTha zNS)KzSz6RCY9V`{Yn_f`1v+$z%YBc`$_Er;dftJMJ-C# zcJOAtamff7zeQ|MO}ik5&y=<>Vpp(y(d#gn`t=1gjmEqpo(b(%-Kp}(;`9D?ZeoQo zHcGDC1Y16mjFEA`4}R3PG4FMBskW&{93ki#_nl6=Wd%k%i=$u6AQ9NhSlh6mdT(P# zQPbn)3ckV4kQ3^=oR#P#=Y)ST06$?WK`rhoIer-IWt8^5{Jyzg;pSWQJkw|ByPW?; z^P>X^8p*B5t11l1F36v;eL-u+w)pL#C+~#UgI_i`w}o@ptj-O+==fE_`qzx(cZ=0K zckeoNv*SKN57*^eWMp)-4fG3pYK#z!Mq)!~-F&SH`8B%vIiFTVNEVuF6gU_}Ogxpi_W zE&krMawV`Mu0gE39IdOgHgg$H(01Vn72nLNetUChu!{MY6nvgGpn2)#qG}tD824kUzy)s4HYE0%rtsItwlUEX)yld{;e@R+& zJqR1J!8m^u4h_ws+Gl%*g>g|%AGB?o&(X8U@9GPVwyBG@SK8=?o;51ww29A{l!j-l z^%c#=?DXH>r~-3Di+bHqF!weJ5uy;w4PjHa7mwHaE!S#Y5(Wy{fQV^W@%SavS=E?U z%ZUHe8*NDcvwQ#ay|s!vehiU>Ja|qIVVH+=q~A>R_2|}f*E|rds#daa@IsEkkc5X7 z#CTPF?DR)^ulO^R1u&K2=}GZ&@|O8nR{aCVp{6fXZyJIMXl$N{8$nep(f0^G!7F+J&wunW4mFM|B&|y7+LaICmXxPegB;s-@6cCEe)n_V=iJZz`{()Z`Q!0Ae~?ozzRUHwuJ?8oS(&zd zR+w~L!uo7(xDVbfgGeRD?pp-^*;0d zI9!Xw$J4ua6dd*vj_m+XW0qyL7)E~^Dutq+Gu!Zd9b`WigL$-8h&)W> zfd!!U_wt>3W_6S$hLJbSw)>E1tAqmd!I8 zQ6a0Cc~7MxR!>u}tt)|)x1v6;*nJA13qU@&9MTy?o;p`tO@|)VcjLs}aTRxz z=EF?rwv>bG+Z}OF5mqhZXtJr6((fqpDvm3-K7^zFr#J@y<>|mOe|s#4UU(c0ZOgwH z2oSCmm&9iQKtM%|UQ*OU=dxAdcZUUN;he3_ScJpp8e+!zKfm{|F8a?WW0QYjh+1p zh{-_VeVFB_D`qaPnLSs((ln}31>BdD`8eIy700rlSO&s>Nb7!#457K6)NJHw2gp1Y zC$C+G(}!)hXHLidYoLM*GAyJ9?w zJt=N1RC8QCE~n}cY?ep49!w(Du5_6Ch8xO_jk9&>+zELd`oGa;C$8eW9tYsqbNA7% zcH$6I%!%ioUfPk_f_2YZTv2b++6MnfeKzv>V($9w?wCH0nn->Dr;Q^F&~2QD(hQ)) z=FbiWdJzaUSVD_+2NG3)_v_|r_O@kl72npV5q!MY9Y$9O` zMC;L}{fl#Wg~_v+i7M);zCkxqQdk)G*62*^GWo(KR>z7x*5;9~O&Xeo-_~mNjCimi zB~JYA=;{q7&}jKqLHQYN7-IpF3chIzAm7Z71YGnMBQ$XyYiSgHPMMg*(z;CI9fA!+2uo{a%^N z<&KiAFF){gpA`7;Y~N%hgaxar-@UyyI~R8Ua*u^a0h0aT3Pz4wt}kJ=<-2D=IjzT} zYHn3OR~J3kt0`NK)@u532ElS9cfNvy|JAO6)2B{l(d6gfCT_QOsa}2&^)AK|qiL?* zY*Lcbm>-p=8LhjEMaV?N<8h=+pui>u&7kM8o|D3{%5U~&U9c)LWa1CZX}DXwaI75S zXU_^bY#Gpr$dge;Ye z#c%FiV|KNec2hnTl z&twyNqZ!dtIhX9W;+4|GJQ7X7Y1&lkk<{p2RX#`kRB)bqY`~U?S$s-mHpb0)cgX=( z#!BxKGWDYe;abJpwZT5HGY;Pw8C<&!uy%418~o9J9ZzC(N7%TF<--!;uvKm%;r1C1W1%Q$(Ux}(`0SC@QhB#8IdSF2IUF7PcG1j_<9CVXD z%QUs7us;?ZdUE!O+}M=td{1zX(%X?LX(t(Lp1+#a3U%=p&mFfsq4Ci$m)>3aylL~d zeY$415xi+KcOwjjnBgWv#RfmJME$q-!-4{BHG-Q#*RPEGxO=|{D^e_nva7;ApZ{Dp zb6v9Z@DY~wnCqi+k#%g{RGs|fl|tRUi$ea^*tH5;&6V9&{4R`Z>Eu(|3Preh@7#yNb;S4!gU zC>t*WcgVry-iebx`w6Yzq|9YlcOLUFA+Awc^3%dkc~#2MM~nFV9Z6LC#q`3mVo6CI z1$6+DAhGhH9ZlS{UQJu@sY6 z(ad5mAj?+Eko1TNV}o|BnntJdx1M5tzVluT^RG6TVK4{#B`Dcn#QugLyA%zTGD6RJ zYL8A*3*tbkR%B6YXudfgo(b;>W)G&P`cP(u<;16l6rHALdpNVbChst7nBQWI=p16K3IVYx*H{aVl6h$B=kQfWj}aoCDQe+;M;qlJ536M;9N9Ktaj9 zTl6ZlWAx&FJc~eK< zIOm20BW45g{|!ejK-9he2DK@FO`o|)sb&}U9I+ytxo$k? zyOFM?)wwt-AmI((f7utJ!xiRlXHR;)<3Q}|J*G0jp!3yeXXqRtG>6qE`>If zYd+P)eB3rS?eTmoP7jw9H!V|3y?~;dA@cf1#OHl>Ka&S3T(2=+mXQAo{j0vvnK*q#q6x8H7xQ- z-CF6o$So_J_yWqZ_$Yr z@#}iNgSel=?x(Nf|5Y9yS3zM5 zReHv(f=nt4US$)|V>@-K(XNP|ZPq{TP7a2@G!-OL1GErL%W>8qJuSnw2gl@)`sS-a z+bIIl5@INhjGXH|07EG4Yfv@mVeQ6tozDxQEUmYWC*~}@nc|$=Qukis)_7Ddj+w5a z-lNKF1y+}D+>*JBD}D4Kw!Clq=cqk;@%>a|jpga1({FKauh2lad{G`#loPy{Kw! zi7%7sbbtL$44V*rN=3yk*3{2DwuuQ(B7M|f=3;}(!$IiDC38uJxpv37vR{bjlCyoE zouJmiku(uW8qia*E2}e;Gk(6l-0>psqI>Q8pRQ) zs9cPJp!5Q32CmTa660R6Upz@4D~9>{*TpAe!^5et0bB)I4oZvcIuM%QHwK1%%UzcR zDY^zrznqdF*8QA(S>w4uQ9+|S0+OI&3`_V$_i2I<<13dCHI`&?zz6uaOY5iHYJ}OC zLQkU1jFXqU)yN|Rihxv!U4uLlW6eC3(1?DKv&V;n-jGrqofPJ&hO-xU$koyuSvamx zA$85mNClE|a|Sf~RIPcl z!6iwr46BY9Imiq-BbBUz&aODDGo7EGAL(`3Xv@#9Xzk;JRSW3lKk$qTwyQJP1+Gs8 z4{X!)FwM&opw${%o4pj$-2#&5lew%8X9643F*juiIzzK1koYGFv#I2VMs@<>agdRlck5pw! zXpx*%oUS0V%Y3;M)tRcQ%$(_IKB&tj>DQ6XOU$f5C?&j^ujTZ`Prc`1H{2S4$9-*c zn3Z3=%x-lYd_Oxp5e7vM#HQ^H4AbSj$*6m*L2F?-em`zv@O*t_Q$B>nwf&l2&9_qU zI=d*IOV_O>K}beJwk{R<%QOByTjJ< zF`HJ=Q$LOBCHKM8zj2<8s&bFYeZ&18kN;T7nu@bVGQ?)UAco7is{C>OWy8s?Jl|)N z#aod$H7g5?$?<)vmymj)oGHO7ar;AGj7|G; zSmy^M_Cf~~`Tp3PzMgT;FrV7wL|(Ym1FrLAYRKH4^^nYV0Zr||+^VsM3REY5UVZXHvsOYBeE84*$+MK+*u z>U^2>06m&+60DwPpij2?^G(Gx=$R6L41C|R+h-!tC|>B=91Fz+VXBJ{TU(;zqVn2z zl+&_@BlR*}qFeNk(zAVQXxeERPJ!AtA$JLo;6^Q;vmL0`WcliZ}SHJZKIg!7NisOUnxBKnU-#4Kye~t!y@aAd~u?%Gw_rQ@D7O^O#Cr)3$yKlt%DLR+PUq#i!1<)bg3~3 zn?XK2nI8nD;<-k#FIyW6Lxla?_+FHpOUJgWj8DJ)0kce&kt_Nk!c0=V@tar-thTzj_ex z)jCHt%W=F>ao)OSzKrL*wm$~9R8){;g6ulm+2e$~i-Gilvjt*q-RIEsU+-hRj)f-4 zdcNb$R(SO6L#(p)p=6}%dZJ|X{>7P;h%WbL1C6cz9H&;t*c*N$`zHT^{%Man3XQs{ z%U_ObX4VpDtG}+#NIUDp>)Nr`b6_Ojq*hYl(7X#~NVpnbZ06;)S~t^nj1s=Uxw6JD zM+o>iZ1{N5X~Zh)k3C(%J4BY->8Z>Px&op-RW;MCEp;vPC++gZi>f^Sa*RDob>+pg zKXNSvC`c@f?%JDMuLE&~SB@k>>twe|XWB&{n``geRE>eH2rpS?%|?woZ-|Y=zjy&< zf{>t%c-x2TQ9`)=IEN_--+m4XZGf zdDFe{s{YL1&COMx;Cp}-VhJ=p-+F2)9Wy$XNVcEzv53D4(-F7-iX(}vaQMiZLT_T~ zwB=kPbl!cv`%QvW_g)|9$k2(`yfn6w&0AYvdbA&@MSaoTTWlZ3Yuo=)kVDcvxv3aW ziPSr9tMjn>5b9?mHHVfaCtF_9S08&g^4^KQ^y4auV)k~w6+*CHHYYJ|BHysPo2xv8 z=Z_O75)`wV8Xjk+8wdhCE+9!mU!c|&bb+na8=E& zPhcZSsJe=t>AST|b5TgBf9w=|B$DhQV^RtjM7EO?W&EHM?lC&xnNO?yC+`m#(089R zqjyL+1+QwpJ1*t2Pg>yU))r)XmtA!PyfzoO0w7B0hla!0|=o&i|3vFJ+oF+buA3IkM=(g^Mlr zJR2M&>_lWXgg_{DK{=XZHBSenKx^el=D$r)%>^xb>c4oAKSc=-*k?N7t6 ze~Mn{b01)QNykUT=pepcMwY9LUoi5`iIwywG;Q5Muu^tO7@SbAEqbMSv*u-EQ!9Az zz0u4U?dSn(MP0m3nB3z)Q=nA1Vw*mQk?{^&h3zP`k?d$vXp-Bh&=T^UzXI5ukQ;z9e*2m6PRdRq@qFT3rQ>9bH z5tgHeyH17;qylo-=SV`=bHkIUsEp9lXSr;n7Xd#~Sf6JcRUDqC?w9weJ07Y~ZR0mK zVFI*tOA9;NRt@=RTQ(cN%RX=CFWha!K&zI-i^y2!un)^LO?CH{PW3k;2x%LxWW&<* z8LE`;nO7k}+KgAu&`{e@5;v88Ql~|(npP=c&A(tgSxuGU+CD$9*#5y1udShxReB`K z_RISro!-cO-i^QGweQ=FSnl0vFp(YO*R7_q&1g=TikO0Nj$3)}qD!}bY9_6Xg@8&& zFO(}qxw(*GK<)6yf&CO%OV+m^NR0KG&}Cl7Ad z)+&|fhFWKQ{CIbrl}q!{ZTsN@6X*-cnStZCG9mm*K*O_x$IOuNr zotZB%>!Tvl(#oyXOQE8o`S5Tve^ruV{RVs&x2Vik|LXL@#ouMOld{*y3Z7+^&bP|N za~D6ENMu{)SPz?{5}KRv%wh=>xAe;)oI0ug&VNtRPgHe~$k)Y=v{9G!kqB*bxfjwT zQ;F>E-D0l~UwFc|G%auOqID04c-N+9f7aI*I7NLpxQ5Qj#fhn1tkkect@470^S**O z*8QhDc5A*PZoE`^yq-%!O6Zd@`|Wjyx~t~>r>{4Sk9$CcW$p7a&lSGrpKTl0rn21U zkb03X?VsTK3~T_2rL#_Mz}4tI^s)Iv~Jx>851_0b@la`iZX>ZM2lwHUuaPa zsdkZR6%{`FeiIYxM*XNzfXxow_BHt_MBDK{?!AMo;8W& z#Sk95)jUgV+WkzDP3NSbWk73}P95|zt0-m#G z`seNqG-17ljn^tX5}nfp$4h)Sb-Lxb637S=s*L09fqs%gOBrAzqTZ#yvTEUrIGGu~ zxYorAN;iRq`Oh=+%5xs8pVKqI(kLEbTAAuJ=ZGGly=<0V{JcmE{wysdZn#s0ly6m% zr?^@zx23dCLsX<@<`O~uDmEfE%J#)^!=m8%l{b$(JVF!wM^QUR^K9h-cWQ`UOjYsG zb5eE@D}{!+I@l=y9LQOkPdXbeK(Il7xqH_bN*clla}4CaQviT+?j^waZsxll?n28k z?EIFpmpQ2y8YI|R9iI*g)34ckBH?|pzTPN5!ZJ(CR0-H=O)H)~<8STxcz(0criULH z`e|x#cVz;>@I3NOG*JYXcSlj_&oZss)eqK-m15TcGVaay)r%Z#%{!S{2qPz3;!|I5 z-BoG*tL&K?w!V{bq{MnUe6hM-PB_emWZBq=chsX)i9=KqbId#G3>n8eNiZU~89mI- z09ZXDyXkQm!TtMP2TDeZe^Z!TLN-iUHVq3143vWVL`RpwUz!(x=S9v?i?cnd2RKr{ z514E9*G0RLcMkMQZ;$p&GhaUVHHg-mX)jiyf`Xk16%k2h$;+A1W={q3P-Gr;*w(_P zeST#WdTzM{PF`h2A*51zt|~ck^8nK{K!DKiqS`D^;>*miZS@@tvW zSzV^??`s?6>efmhmB@g)c$`B5lhq{rgB%d(>*KSya?vx+G$0cioKCqdEv%Twh(;|* zoLGE@%W}X>eZH3a?;-iz#^weXMXrH?E`}g{(>#Z3$MIr6HFyK#{Ra?WgND?fF9RV@BZ+G%02`Ev&g~BB+nGcogN9}$+8K8HFX!>H% zWGq`~=_ggaTsMBXe7pY3GkukMMjq4M&prFKhs|}ro

f%V7uGs(wyc>auK*eV<_` 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

3V5E~jAhHQ+cVKqKXnu4BFt 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

t!Z6+=<8K#8%!47mG^DvwjN)czHY&MWC>je! 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

h^hxmgz$Ft_`QwMsX;4fvw5QLj$|4P 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%0D_t`u}kWJADAO|4tw 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

HzO#c=-et<%Jhpr@0rTp!s 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 EANjQvzW@LL literal 0 HcmV?d00001 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 72f027f0e91f..1094fd7348d2 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -122,7 +122,8 @@ "plugins/log-rotate", "plugins/error-log-logger", "plugins/sls-logger", - "plugins/google-cloud-logging" + "plugins/google-cloud-logging", + "plugins/splunk-hec-logging" ] }, { diff --git a/docs/en/latest/plugins/splunk-hec-logging.md b/docs/en/latest/plugins/splunk-hec-logging.md new file mode 100644 index 000000000000..2d633293359f --- /dev/null +++ b/docs/en/latest/plugins/splunk-hec-logging.md @@ -0,0 +1,143 @@ +--- +title: splunk-hec-logging +--- + + + +## Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +The `splunk-hec-logging` plugin is used to forward the request log of `Apache APISIX` to `Splunk HTTP Event Collector (HEC)` for analysis and storage. After the plugin is enabled, `Apache APISIX` will obtain request context information in `Log Phase` serialize it into [Splunk Event Data format](https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector#Event_metadata) and submit it to the batch queue. When the maximum processing capacity of each batch of the batch processing queue or the maximum time to refresh the buffer is triggered, the data in the queue will be submitted to `Splunk HEC`. + +For more info on Batch-Processor in Apache APISIX please refer to: +[Batch-Processor](../batch-processor.md) + +## Attributes + +| Name | Requirement | Default | Description | +| ----------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| endpoint | required | | Splunk HEC endpoint configuration info | +| endpoint.uri | required | | Splunk HEC event collector API | +| endpoint.token | required | | Splunk HEC authentication token | +| endpoint.channel | optional | | Splunk HEC send data channel identifier, refer to: [About HTTP Event Collector Indexer Acknowledgment](https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/AboutHECIDXAck) | +| endpoint.timeout | optional | 10 | Splunk HEC send data timeout, time unit: (seconds) | +| ssl_verify | optional | true | enable `SSL` verification, option as per [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) | +| max_retry_count | optional | 0 | max number of retries before removing from the processing pipe line | +| retry_delay | optional | 1 | number of seconds the process execution should be delayed if the execution fails | +| buffer_duration | optional | 60 | max age in seconds of the oldest entry in a batch before the batch must be processed | +| inactive_timeout | optional | 5 | max age in seconds when the buffer will be flushed if inactive | +| batch_max_size | optional | 1000 | max size of each batch | + +## How To Enable + +The following is an example of how to enable the `splunk-hec-logging` for a specific route. + +### Full configuration + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins":{ + "splunk-hec-logging":{ + "endpoint":{ + "uri":"http://127.0.0.1:8088/services/collector", + "token":"BD274822-96AA-4DA6-90EC-18940FB2414C", + "channel":"FE0ECFAD-13D5-401B-847D-77833BD77131", + "timeout":60 + }, + "buffer_duration":60, + "max_retry_count":0, + "retry_delay":1, + "inactive_timeout":2, + "batch_max_size":10 + } + }, + "upstream":{ + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + }, + "uri":"/splunk.do" +}' +``` + +### Minimize configuration + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins":{ + "splunk-hec-logging":{ + "endpoint":{ + "uri":"http://127.0.0.1:8088/services/collector", + "token":"BD274822-96AA-4DA6-90EC-18940FB2414C" + } + } + }, + "upstream":{ + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + }, + "uri":"/splunk.do" +}' +``` + +## Test Plugin + +* Send request to route configured with the `splunk-hec-logging` plugin + +```shell +$ curl -i http://127.0.0.1:9080/splunk.do?q=hello +HTTP/1.1 200 OK +... +hello, world +``` + +* Login to Splunk Dashboard to search and view + +![splunk hec search view](../../../assets/images/plugin/splunk-hec-admin-en.png) + +## Disable Plugin + +Disabling the `splunk-hec-logging` plugin is very simple, just remove the `JSON` configuration corresponding to `splunk-hec-logging`. + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index b2e43c2049a7..88d46fd2d929 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -135,7 +135,7 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - 高性能:在单核上 QPS 可以达到 18k,同时延迟只有 0.2 毫秒。 - [故障注入](plugins/fault-injection.md) - [REST Admin API](admin-api.md): 使用 REST Admin API 来控制 Apache APISIX,默认只允许 127.0.0.1 访问,你可以修改 `conf/config.yaml` 中的 `allow_admin` 字段,指定允许调用 Admin API 的 IP 列表。同时需要注意的是,Admin API 使用 key auth 来校验调用者身份,**在部署前需要修改 `conf/config.yaml` 中的 `admin_key` 字段,来保证安全。** - - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md), [TCP Logger](plugins/tcp-logger.md), [Kafka Logger](plugins/kafka-logger.md), [UDP Logger](plugins/udp-logger.md), [RocketMQ Logger](plugins/rocketmq-logger.md), [SkyWalking Logger](plugins/skywalking-logger.md), [Alibaba Cloud Logging(SLS)](plugins/sls-logger.md), [Google Cloud Logging](plugins/google-cloud-logging.md)) + - 外部日志记录器:将访问日志导出到外部日志管理工具。([HTTP Logger](plugins/http-logger.md)、[TCP Logger](plugins/tcp-logger.md)、[Kafka Logger](plugins/kafka-logger.md)、[UDP Logger](plugins/udp-logger.md)、[RocketMQ Logger](plugins/rocketmq-logger.md)、[SkyWalking Logger](plugins/skywalking-logger.md)、[Alibaba Cloud Logging(SLS)](plugins/sls-logger.md)、[Google Cloud Logging](plugins/google-cloud-logging.md)、[Splunk HEC Logging](plugins/splunk-hec-logging.md)) - [Helm charts](https://github.com/apache/apisix-helm-chart) - **高度可扩展** diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index cd914b7a4700..7ac810ca42d9 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -120,7 +120,8 @@ "plugins/log-rotate", "plugins/error-log-logger", "plugins/sls-logger", - "plugins/google-cloud-logging" + "plugins/google-cloud-logging", + "plugins/splunk-hec-logging" ] }, { diff --git a/docs/zh/latest/plugins/splunk-hec-logging.md b/docs/zh/latest/plugins/splunk-hec-logging.md new file mode 100644 index 000000000000..719a581b973a --- /dev/null +++ b/docs/zh/latest/plugins/splunk-hec-logging.md @@ -0,0 +1,143 @@ +--- +title: splunk-hec-logging +--- + + + +## 摘要 + +- [**定义**](#定义) +- [**属性列表**](#属性列表) +- [**如何开启**](#如何开启) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 定义 + +`splunk-hec-logging` 插件用于将 `Apache APISIX` 的请求日志转发到 `Splunk HTTP 事件收集器(HEC)` 中进行分析和存储,启用该插件后 `Apache APISIX` 将在 `Log Phase` 获取请求上下文信息并序列化为 [Splunk Event Data 格式](https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector#Event_metadata) 后提交到批处理队列中,当触发批处理队列每批次最大处理容量或刷新缓冲区的最大时间时会将队列中的数据提交到 `Splunk HEC` 中。 + +有关 `Apache APISIX` 的 `Batch-Processor` 的更多信息,请参考: +[Batch-Processor](../batch-processor.md) + +## 属性列表 + +| 名称 | 是否必需 | 默认值 | 描述 | +| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| endpoint | 必选 | | Splunk HEC 端点配置信息 | +| endpoint.uri | 必选 | | Splunk HEC 事件收集API | +| endpoint.token | 必选 | | Splunk HEC 身份令牌 | +| endpoint.channel | 可选 | | Splunk HEC 发送渠道标识,参考:[About HTTP Event Collector Indexer Acknowledgment](https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/AboutHECIDXAck) | +| endpoint.timeout | 可选 | 10 | Splunk HEC 数据提交超时时间(以秒为单位) | +| ssl_verify | 可选 | true | 启用 `SSL` 验证, 参考:[OpenResty文档](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) | +| max_retry_count | 可选 | 0 | 从处理管道中移除之前的最大重试次数 | +| retry_delay | 可选 | 1 | 如果执行失败,流程执行应延迟的秒数 | +| buffer_duration | 可选 | 60 | 必须先处理批次中最旧条目的最大期限(以秒为单位) | +| inactive_timeout | 可选 | 5 | 刷新缓冲区的最大时间(以秒为单位) | +| batch_max_size | 可选 | 1000 | 每个批处理队列可容纳的最大条目数 | + +## 如何开启 + +下面例子展示了如何为指定路由开启 `splunk-hec-logging` 插件。 + +### 完整配置 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins":{ + "splunk-hec-logging":{ + "endpoint":{ + "uri":"http://127.0.0.1:8088/services/collector", + "token":"BD274822-96AA-4DA6-90EC-18940FB2414C", + "channel":"FE0ECFAD-13D5-401B-847D-77833BD77131", + "timeout":60 + }, + "buffer_duration":60, + "max_retry_count":0, + "retry_delay":1, + "inactive_timeout":2, + "batch_max_size":10 + } + }, + "upstream":{ + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + }, + "uri":"/splunk.do" +}' +``` + +### 最小化配置 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins":{ + "splunk-hec-logging":{ + "endpoint":{ + "uri":"http://127.0.0.1:8088/services/collector", + "token":"BD274822-96AA-4DA6-90EC-18940FB2414C" + } + } + }, + "upstream":{ + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + }, + "uri":"/splunk.do" +}' +``` + +## 测试插件 + +* 向配置 `splunk-hec-logging` 插件的路由发送请求 + +```shell +$ curl -i http://127.0.0.1:9080/splunk.do?q=hello +HTTP/1.1 200 OK +... +hello, world +``` + +* 登录Splunk控制台检索查看日志 + +![splunk hec search view](../../../assets/images/plugin/splunk-hec-admin-cn.png) + +## 禁用插件 + +禁用 `splunk-hec-logging` 插件非常简单,只需将 `splunk-hec-logging` 对应的 `JSON` 配置移除即可。 + +```shell +$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/hello", + "plugins": {}, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index c0806346528c..2495b5395185 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -102,6 +102,7 @@ prometheus datadog echo http-logger +splunk-hec-logging skywalking-logger google-cloud-logging sls-logger diff --git a/t/plugin/splunk-hec-logging.t b/t/plugin/splunk-hec-logging.t new file mode 100644 index 000000000000..22d38ec2fb93 --- /dev/null +++ b/t/plugin/splunk-hec-logging.t @@ -0,0 +1,198 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + +}); + +run_tests(); + +__DATA__ + +=== TEST 1: configuration verification +--- config + location /t { + content_by_lua_block { + local ok, err + local configs = { + -- full configuration + { + endpoint = { + uri = "http://127.0.0.1:18088/services/collector", + token = "BD274822-96AA-4DA6-90EC-18940FB2414C", + channel = "FE0ECFAD-13D5-401B-847D-77833BD77131", + timeout = 60 + }, + max_retry_count = 0, + retry_delay = 1, + buffer_duration = 60, + inactive_timeout = 2, + batch_max_size = 10, + }, + -- minimize configuration + { + endpoint = { + uri = "http://127.0.0.1:18088/services/collector", + token = "BD274822-96AA-4DA6-90EC-18940FB2414C", + } + }, + -- property "uri" is required + { + endpoint = { + token = "BD274822-96AA-4DA6-90EC-18940FB2414C", + } + }, + -- property "token" is required + { + endpoint = { + uri = "http://127.0.0.1:18088/services/collector", + } + }, + -- property "uri" validation failed + { + endpoint = { + uri = "127.0.0.1:18088/services/collector", + token = "BD274822-96AA-4DA6-90EC-18940FB2414C", + } + } + } + + local plugin = require("apisix.plugins.splunk-hec-logging") + for i = 1, #configs do + ok, err = plugin.check_schema(configs[i]) + if err then + ngx.say(err) + else + ngx.say("passed") + end + end + } + } +--- response_body_like +passed +passed +property "endpoint" validation failed: property "uri" is required +property "endpoint" validation failed: property "token" is required +property "endpoint" validation failed: property "uri" validation failed.* + + + +=== TEST 2: set route (failed auth) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["splunk-hec-logging"] = { + endpoint = { + uri = "http://127.0.0.1:18088/services/collector", + token = "BD274822-96AA-4DA6-90EC-18940FB24444" + }, + batch_max_size = 1, + inactive_timeout = 1 + } + } + }) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: test route (failed auth) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world +--- error_log +Batch Processor[splunk-hec-logging] failed to process entries: failed to send splunk, Invalid token +Batch Processor[splunk-hec-logging] exceeded the max_retry_count + + + +=== TEST 4: set route (success write) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["splunk-hec-logging"] = { + endpoint = { + uri = "http://127.0.0.1:18088/services/collector", + token = "BD274822-96AA-4DA6-90EC-18940FB2414C" + }, + batch_max_size = 1, + inactive_timeout = 1 + } + } + }) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: test route (success write) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world From 15128f0a56c5950ab616c9df54a3df4269765445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Mon, 20 Dec 2021 19:20:31 +0800 Subject: [PATCH 207/260] fix(google-cloud-logging): move ssl_verify to plugin configuration top level (#5839) --- apisix/plugins/google-cloud-logging.lua | 70 +++++---- apisix/plugins/google-cloud-logging/oauth.lua | 8 +- .../en/latest/plugins/google-cloud-logging.md | 2 +- .../zh/latest/plugins/google-cloud-logging.md | 2 +- t/plugin/google-cloud-logging.t | 146 ++++++++++++++++++ .../config-https-domain.json | 9 ++ .../google-cloud-logging/config-https-ip.json | 9 ++ 7 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 t/plugin/google-cloud-logging/config-https-domain.json create mode 100644 t/plugin/google-cloud-logging/config-https-ip.json diff --git a/apisix/plugins/google-cloud-logging.lua b/apisix/plugins/google-cloud-logging.lua index 0b34e2223031..a28e7f62d463 100644 --- a/apisix/plugins/google-cloud-logging.lua +++ b/apisix/plugins/google-cloud-logging.lua @@ -24,8 +24,9 @@ local bp_manager_mod = require("apisix.utils.batch-processor-manager") local google_oauth = require("apisix.plugins.google-cloud-logging.oauth") -local auth_config_cache - +local lrucache = core.lrucache.new({ + type = "plugin", +}) local plugin_name = "google-cloud-logging" local batch_processor_manager = bp_manager_mod.new(plugin_name) @@ -61,13 +62,13 @@ local schema = { type = "string", default = "https://logging.googleapis.com/v2/entries:write" }, - ssl_verify = { - type = "boolean", - default = true - }, }, required = { "private_key", "project_id", "token_uri" } }, + ssl_verify = { + type = "boolean", + default = true + }, auth_file = { type = "string" }, -- https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource resource = { @@ -136,42 +137,41 @@ local function send_to_google(oauth, entries) end -local function get_auth_config(config) - if auth_config_cache then - return auth_config_cache +local function fetch_oauth_conf(conf) + if conf.auth_config then + return conf.auth_config end - if config.auth_config then - auth_config_cache = config.auth_config - return auth_config_cache - end - - if not config.auth_file then + if not conf.auth_file then return nil, "configuration is not defined" end - local file_content, err = core.io.get_file(config.auth_file) + local file_content, err = core.io.get_file(conf.auth_file) if not file_content then - return nil, "failed to read configuration, file: " .. config.auth_file .. " err: " .. err + return nil, "failed to read configuration, file: " .. conf.auth_file .. " err: " .. err end - local config_data - config_data, err = core.json.decode(file_content) - if not config_data then + local config_tab + config_tab, err = core.json.decode(file_content) + if not config_tab then return nil, "config parse failure, data: " .. file_content .. " , err: " .. err end - auth_config_cache = config_data - return auth_config_cache + return config_tab end -local function get_logger_entry(conf, ctx) - local auth_config, err = get_auth_config(conf) - if err or not auth_config.project_id or not auth_config.private_key then - return nil, "failed to get google authentication configuration, " .. err +local function create_oauth_object(conf) + local auth_conf, err = fetch_oauth_conf(conf) + if not auth_conf then + return nil, err end + return google_oauth:new(auth_conf, conf.ssl_verify) +end + + +local function get_logger_entry(conf, ctx, oauth) local entry = log_util.get_full_log(ngx, conf) local google_entry = { httpRequest = { @@ -195,8 +195,8 @@ local function get_logger_entry(conf, ctx) timestamp = log_util.get_rfc3339_zulu_timestamp(), resource = conf.resource, insertId = ctx.var.request_id, - logName = core.string.format("projects/%s/logs/%s", auth_config_cache.project_id, - conf.log_id) + logName = core.string.format("projects/%s/logs/%s", oauth.project_id, + conf.log_id) } return google_entry @@ -217,7 +217,15 @@ end function _M.log(conf, ctx) - local entry, err = get_logger_entry(conf, ctx) + local oauth, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, + create_oauth_object, conf) + if not oauth then + core.log.error("failed to fetch google-cloud-logging.oauth object: ", err) + return + end + + local entry + entry, err = get_logger_entry(conf, ctx, oauth) if err then core.log.error(err) return @@ -227,10 +235,8 @@ function _M.log(conf, ctx) return end - local oauth_client = google_oauth:new(auth_config_cache) - local process = function(entries) - return send_to_google(oauth_client, entries) + return send_to_google(oauth, entries) end batch_processor_manager:add_entry_to_new_processor(conf, entry, ctx, process) diff --git a/apisix/plugins/google-cloud-logging/oauth.lua b/apisix/plugins/google-cloud-logging/oauth.lua index 33f83be6b48a..a560bd43f7dd 100644 --- a/apisix/plugins/google-cloud-logging/oauth.lua +++ b/apisix/plugins/google-cloud-logging/oauth.lua @@ -98,7 +98,7 @@ function _M:generate_jwt_token() end -function _M:new(config) +function _M:new(config, ssl_verify) local oauth = { client_email = config.client_email, private_key = config.private_key, @@ -111,11 +111,7 @@ function _M:new(config) access_token_expire_time = 0, } - if config.ssl_verify == false then - oauth.ssl_verify = config.ssl_verify - else - oauth.ssl_verify = true - end + oauth.ssl_verify = ssl_verify if config.scopes then if type(config.scopes) == "string" then diff --git a/docs/en/latest/plugins/google-cloud-logging.md b/docs/en/latest/plugins/google-cloud-logging.md index 6fe84b4a5cf7..5fe10d5d8ef9 100644 --- a/docs/en/latest/plugins/google-cloud-logging.md +++ b/docs/en/latest/plugins/google-cloud-logging.md @@ -48,8 +48,8 @@ For more info on Batch-Processor in Apache APISIX please refer: | auth_config.token_uri | optional | https://oauth2.googleapis.com/token | the token uri parameters of the Google service account | | auth_config.entries_uri | optional | https://logging.googleapis.com/v2/entries:write | google cloud logging service API | | auth_config.scopes | optional | ["https://www.googleapis.com/auth/logging.read","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/logging.admin","https://www.googleapis.com/auth/cloud-platform"] | the access scopes parameters of the Google service account, refer to: [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#logging) | -| auth_config.ssl_verify | optional | true | enable `SSL` verification, option as per [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) | | auth_file | semi-optional | | path to the google service account json file(Semi-optional, one of auth_config or auth_file must be configured) | +| ssl_verify | optional | true | enable `SSL` verification, option as per [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) | | resource | optional | {"type": "global"} | the Google monitor resource, refer to: [MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource) | | log_id | optional | apisix.apache.org%2Flogs | google cloud logging id, refer to: [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) | | max_retry_count | optional | 0 | max number of retries before removing from the processing pipe line | diff --git a/docs/zh/latest/plugins/google-cloud-logging.md b/docs/zh/latest/plugins/google-cloud-logging.md index 80bc7c5cdcca..b094b7b066d9 100644 --- a/docs/zh/latest/plugins/google-cloud-logging.md +++ b/docs/zh/latest/plugins/google-cloud-logging.md @@ -48,8 +48,8 @@ title: google-cloud-logging | auth_config.token_uri | 可选 | https://oauth2.googleapis.com/token | 请求谷歌服务帐户的令牌的URI | | auth_config.entries_uri | 可选 | https://logging.googleapis.com/v2/entries:write | 谷歌日志服务写入日志条目的API | | auth_config.scopes | 可选 | ["https://www.googleapis.com/auth/logging.read","https://www.googleapis.com/auth/logging.write","https://www.googleapis.com/auth/logging.admin","https://www.googleapis.com/auth/cloud-platform"] | 谷歌服务账号的访问范围, 参考: [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes#logging) | -| auth_config.ssl_verify | 可选 | true | 启用 `SSL` 验证, 配置根据 [OpenResty文档](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) 选项| | auth_file | 半可选 | | 谷歌服务账号JSON文件的路径(必须配置 `auth_config` 或 `auth_file` 之一) | +| ssl_verify | 可选 | true | 启用 `SSL` 验证, 配置根据 [OpenResty文档](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake) 选项| | resource | 可选 | {"type": "global"} | 谷歌监控资源,参考: [MonitoredResource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource) | | log_id | 可选 | apisix.apache.org%2Flogs | 谷歌日志ID,参考: [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) | | max_retry_count | 可选 | 0 | 从处理管道中移除之前的最大重试次数 | diff --git a/t/plugin/google-cloud-logging.t b/t/plugin/google-cloud-logging.t index 202e28c4398e..d790b9f11cc8 100644 --- a/t/plugin/google-cloud-logging.t +++ b/t/plugin/google-cloud-logging.t @@ -605,3 +605,149 @@ GET /hello hello world --- error_log config.json: No such file or directory + + + +=== TEST 20: set route (https file configuration is successful) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_file = "t/plugin/google-cloud-logging/config-https-domain.json", + inactive_timeout = 1, + batch_max_size = 1, + ssl_verify = true, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 21: test route(https file configuration is successful) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world + + + +=== TEST 22: set route (https file configuration SSL authentication failed: ssl_verify = true) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_file = "t/plugin/google-cloud-logging/config-https-ip.json", + inactive_timeout = 1, + batch_max_size = 1, + ssl_verify = true, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 23: test route(https file configuration SSL authentication failed: ssl_verify = true) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world +--- error_log +failed to refresh google oauth access token, certificate host mismatch + + + +=== TEST 24: set route (https file configuration SSL authentication succeed: ssl_verify = false) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_file = "t/plugin/google-cloud-logging/config-https-ip.json", + inactive_timeout = 1, + batch_max_size = 1, + ssl_verify = false, + } + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 25: test route(https file configuration SSL authentication succeed: ssl_verify = false) +--- request +GET /hello +--- wait: 2 +--- response_body +hello world diff --git a/t/plugin/google-cloud-logging/config-https-domain.json b/t/plugin/google-cloud-logging/config-https-domain.json new file mode 100644 index 000000000000..3f76541146a0 --- /dev/null +++ b/t/plugin/google-cloud-logging/config-https-domain.json @@ -0,0 +1,9 @@ +{ + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv\n0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7\n+pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL\nwQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF\nIeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb\n2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs\nYvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG\n-----END RSA PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "https://test.com:1983/google/logging/token", + "scopes": [ + "https://apisix.apache.org/logs:admin" + ], + "entries_uri": "https://test.com:1983/google/logging/entries" +} diff --git a/t/plugin/google-cloud-logging/config-https-ip.json b/t/plugin/google-cloud-logging/config-https-ip.json new file mode 100644 index 000000000000..18f8e33b9a66 --- /dev/null +++ b/t/plugin/google-cloud-logging/config-https-ip.json @@ -0,0 +1,9 @@ +{ + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKebDxlvQMGyEesAL1r1nIJBkSdqu3Hr7noq/0ukiZqVQLSJPMOv\n0oxQSutvvK3hoibwGakDOza+xRITB7cs2cECAwEAAQJAYPWh6YvjwWobVYC45Hz7\n+pqlt1DWeVQMlN407HSWKjdH548ady46xiQuZ5Cfx3YyCcnsfVWaQNbC+jFbY4YL\nwQIhANfASwz8+2sKg1xtvzyaChX5S5XaQTB+azFImBJumixZAiEAxt93Td6JH1RF\nIeQmD/K+DClZMqSrliUzUqJnCPCzy6kCIAekDsRh/UF4ONjAJkKuLedDUfL3rNFb\n2M4BBSm58wnZAiEAwYLMOg8h6kQ7iMDRcI9I8diCHM8yz0SfbfbsvzxIFxECICXs\nYvIufaZvBa8f+E/9CANlVhm5wKAyM8N8GJsiCyEG\n-----END RSA PRIVATE KEY-----", + "project_id": "apisix", + "token_uri": "https://127.0.0.1:1983/google/logging/token", + "scopes": [ + "https://apisix.apache.org/logs:admin" + ], + "entries_uri": "https://127.0.0.1:1983/google/logging/entries" +} From 94891b91d102ef9cc79bba540c07eabade6e1ed2 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Mon, 20 Dec 2021 19:21:45 +0800 Subject: [PATCH 208/260] fix(core.log): log level need to be updated in some scenario (#5840) --- apisix/core/log.lua | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/apisix/core/log.lua b/apisix/core/log.lua index c94d95d3cf49..a39911390e79 100644 --- a/apisix/core/log.lua +++ b/apisix/core/log.lua @@ -24,6 +24,8 @@ local tostring = tostring local unpack = unpack -- avoid loading other module since core.log is the most foundational one local tab_clear = require("table.clear") +local ngx_errlog = require("ngx.errlog") +local ngx_get_phase = ngx.get_phase local _M = {version = 0.4} @@ -42,17 +44,27 @@ local log_levels = { } -local cur_level = ngx.config.subsystem == "http" and - require "ngx.errlog" .get_sys_filter_level() +local cur_level + local do_nothing = function() end +local function update_log_level() + -- Nginx use `notice` level in init phase instead of error_log directive config + -- Ref to src/core/ngx_log.c's ngx_log_init + if ngx_get_phase() ~= "init" then + cur_level = ngx.config.subsystem == "http" and ngx_errlog.get_sys_filter_level() + end +end + + function _M.new(prefix) local m = {version = _M.version} setmetatable(m, {__index = function(self, cmd) local log_level = log_levels[cmd] - local method + update_log_level() + if cur_level and (log_level > cur_level) then method = do_nothing @@ -64,7 +76,10 @@ function _M.new(prefix) -- cache the lazily generated method in our -- module table - m[cmd] = method + if ngx_get_phase() ~= "init" then + self[cmd] = method + end + return method end}) @@ -74,8 +89,9 @@ end setmetatable(_M, {__index = function(self, cmd) local log_level = log_levels[cmd] - local method + update_log_level() + if cur_level and (log_level > cur_level) then method = do_nothing @@ -87,7 +103,10 @@ setmetatable(_M, {__index = function(self, cmd) -- cache the lazily generated method in our -- module table - _M[cmd] = method + if ngx_get_phase() ~= "init" then + self[cmd] = method + end + return method end}) From 8111f5161d7b00cafd5fc108433cb4d5771134e5 Mon Sep 17 00:00:00 2001 From: Carl Chen Date: Tue, 21 Dec 2021 09:29:38 +0800 Subject: [PATCH 209/260] docs: fix typo in architecture-design document. (#5864) --- docs/assets/images/flow-plugin-internal.png | Bin 121097 -> 123113 bytes .../assets/other/apisix-plugin-design.graffle | Bin 12379 -> 141620 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/assets/images/flow-plugin-internal.png b/docs/assets/images/flow-plugin-internal.png index b41d7ba0398191164010a22ad7e0d5b2a1b133a9..e6ddb1ca00bc24e468a334bb68ad0b4f037d36cb 100644 GIT binary patch literal 123113 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

Ek_`z&Ie8;rA$4kNuo3U{ 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

J6x`ufMRV>%4r6 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 N_jQc5-)lNW{~s7@L|y;@ literal 121097 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

    OV9uPkY%y?yt(c|hQ6M#b+@$r$ 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

    i_&>}0xE>5>9Z5sA-1I2 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 literal 12379 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 RrK2xoO4r#qr6W*~{|CAEAy)tZ From e29ab837a913629c473cdc5a74c29831bd6e6a14 Mon Sep 17 00:00:00 2001 From: Carl Chen Date: Tue, 21 Dec 2021 09:35:10 +0800 Subject: [PATCH 210/260] docs(request-id): fix typo in request-id plugin zh document. (#5863) --- docs/zh/latest/plugins/request-id.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/latest/plugins/request-id.md b/docs/zh/latest/plugins/request-id.md index 87ab8633af2c..f96fd44d7708 100644 --- a/docs/zh/latest/plugins/request-id.md +++ b/docs/zh/latest/plugins/request-id.md @@ -38,7 +38,7 @@ title: request-id | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ------------------- | ------- | -------- | -------------- | ------ | ------------------------------ | | header_name | string | 可选 | "X-Request-Id" | | Request ID header name | -| include_in_response | boolean | 可选 | false | | 是否需要在返回头中包含该唯一ID | +| include_in_response | boolean | 可选 | true | | 是否需要在返回头中包含该唯一ID | | algorithm | string | 可选 | "uuid" | ["uuid", "snowflake"] | ID 生成算法 | ## 如何启用 From 41b55a99705343b79f5b5e4c5e0fe3238d068f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 22 Dec 2021 11:16:08 +0800 Subject: [PATCH 211/260] docs: remove duplicate words in FAQ (#5875) --- docs/en/latest/FAQ.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md index 9f56094208e0..615b2e7b86c0 100644 --- a/docs/en/latest/FAQ.md +++ b/docs/en/latest/FAQ.md @@ -405,8 +405,6 @@ In route, we can achieve more condition matching by combining `uri` with `vars` ## Does the upstream node support configuring the [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) address -This is supported. Here is an example where the `FQDN` is `httpbin.default.svc.cluster.local`: - This is supported. Here is an example where the `FQDN` is `httpbin.default.svc.cluster.local` (a Kubernetes Service): ```shell From a1024075103ca4107b04db3f1258b860f9c823cd Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Wed, 22 Dec 2021 11:17:01 +0800 Subject: [PATCH 212/260] chore: improve installation experience (#5859) --- utils/install-dependencies.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh index b2bd4dfc8614..9669a81c0489 100755 --- a/utils/install-dependencies.sh +++ b/utils/install-dependencies.sh @@ -26,7 +26,7 @@ function detect_aur_helper() { AUR_HELPER=pacaur else echo No available AUR helpers found. Please specify your AUR helper by AUR_HELPER. - exit -1 + exit 255 fi } @@ -49,7 +49,7 @@ function install_dependencies_with_yum() { local common_dep="curl git gcc openresty-openssl111-devel unzip pcre pcre-devel openldap-devel" if [ "${1}" == "centos" ]; then # add APISIX source - sudo yum-config-manager --add-repo https://repos.apiseven.com/packages/centos/apache-apisix.repo + sudo yum install -y https://repos.apiseven.com/packages/centos/apache-apisix-repo-1.0-1.noarch.rpm # install apisix-base and some compilation tools # shellcheck disable=SC2086 From 3c0b374808903388f5facc081dbdb2db672c7c69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Dec 2021 17:22:48 +0800 Subject: [PATCH 213/260] chore(deps): bump actions/setup-go from 2.1.4 to 2.1.5 (#5880) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/chaos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 825c6ddd323b..69fe2737d9be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: submodules: recursive - name: Setup Go - uses: actions/setup-go@v2.1.4 + uses: actions/setup-go@v2.1.5 with: go-version: "1.15" diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 470b548fd7a9..29af3af83415 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -21,7 +21,7 @@ jobs: submodules: recursive - name: Setup go - uses: actions/setup-go@v2.1.4 + uses: actions/setup-go@v2.1.5 with: go-version: "1.16" From ce5bb7b9125356e4c77fb70900d54642a14553a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 23 Dec 2021 14:57:16 +0800 Subject: [PATCH 214/260] change: enable HTTP when stream proxy is set and enable_admin is true (#5867) Co-authored-by: leslie <59061168+leslie-tsang@users.noreply.github.com> --- apisix/cli/ngx_tpl.lua | 2 +- docs/en/latest/stream-proxy.md | 5 ++++- docs/zh/latest/stream-proxy.md | 5 ++++- t/cli/test_core_config.sh | 4 ++-- t/cli/test_dns.sh | 4 ++-- t/cli/test_stream_config.sh | 18 ++++++++++++++++++ 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 66c18372bfc8..20ba36dd6a2c 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -155,7 +155,7 @@ stream { } {% end %} -{% if not (stream_proxy and stream_proxy.only ~= false) then %} +{% if enable_admin or not (stream_proxy and stream_proxy.only ~= false) then %} http { # put extra_lua_path in front of the builtin path # so user can override the source code diff --git a/docs/en/latest/stream-proxy.md b/docs/en/latest/stream-proxy.md index 4b32a001c121..078e3745be34 100644 --- a/docs/en/latest/stream-proxy.md +++ b/docs/en/latest/stream-proxy.md @@ -41,10 +41,13 @@ apisix: - "127.0.0.1:9211" ``` -If you need to enable both HTTP and stream proxy, set the `only` to false: +If `apisix.enable_admin` is true, both HTTP and stream proxy are enabled with the configuration above. + +If you have set the `enable_admin` to false, and need to enable both HTTP and stream proxy, set the `only` to false: ```yaml apisix: + enable_admin: false stream_proxy: # TCP/UDP proxy only: false tcp: # TCP proxy address list diff --git a/docs/zh/latest/stream-proxy.md b/docs/zh/latest/stream-proxy.md index 8039b1cef87e..9b0d8400f95e 100644 --- a/docs/zh/latest/stream-proxy.md +++ b/docs/zh/latest/stream-proxy.md @@ -40,10 +40,13 @@ apisix: - "127.0.0.1:9211" ``` -如果你需要同时启用 HTTP 和 stream 代理,设置 `only` 为 false: +如果 `apisix.enable_admin` 为 true,上面的配置会同时启用 HTTP 和 stream 代理。 + +如果你设置 `enable_admin` 为 false,且需要同时启用 HTTP 和 stream 代理,设置 `only` 为 false: ```yaml apisix: + enable_admin: false stream_proxy: # TCP/UDP proxy only: false tcp: # TCP proxy address list diff --git a/t/cli/test_core_config.sh b/t/cli/test_core_config.sh index be1937851e17..7b96820539cc 100755 --- a/t/cli/test_core_config.sh +++ b/t/cli/test_core_config.sh @@ -56,7 +56,7 @@ nginx_config: make init count=$(grep -c "lua_max_pending_timers 10240;" conf/nginx.conf) -if [ "$count" -ne 1 ]; then +if [ "$count" -ne 2 ]; then echo "failed: failed to set lua_max_pending_timers in stream proxy" exit 1 fi @@ -64,7 +64,7 @@ fi echo "passed: set lua_max_pending_timers successfully in stream proxy" count=$(grep -c "lua_max_running_timers 2561;" conf/nginx.conf) -if [ "$count" -ne 1 ]; then +if [ "$count" -ne 2 ]; then echo "failed: failed to set lua_max_running_timers in stream proxy" exit 1 fi diff --git a/t/cli/test_dns.sh b/t/cli/test_dns.sh index 38c6d7a24970..62985eacc9ce 100755 --- a/t/cli/test_dns.sh +++ b/t/cli/test_dns.sh @@ -53,7 +53,7 @@ apisix: make init count=$(grep -c "resolver 127.0.0.1 \[::1\]:5353 valid=30;" conf/nginx.conf) -if [ "$count" -ne 1 ]; then +if [ "$count" -ne 2 ]; then echo "failed: dns_resolver_valid doesn't take effect" exit 1 fi @@ -74,7 +74,7 @@ apisix: make init count=$(grep -c "resolver 127.0.0.1 \[::1\] \[::2\];" conf/nginx.conf) -if [ "$count" -ne 1 ]; then +if [ "$count" -ne 2 ]; then echo "failed: can't handle IPv6 resolver w/o bracket" exit 1 fi diff --git a/t/cli/test_stream_config.sh b/t/cli/test_stream_config.sh index 1e0cd2a39f1d..71e2f336ac7a 100755 --- a/t/cli/test_stream_config.sh +++ b/t/cli/test_stream_config.sh @@ -21,6 +21,7 @@ echo " apisix: + enable_admin: false stream_proxy: tcp: - addr: 9100 @@ -38,6 +39,7 @@ echo "passed: enable stream proxy only by default" echo " apisix: + enable_admin: false stream_proxy: only: false tcp: @@ -52,6 +54,22 @@ if [ "$count" -ne 2 ]; then exit 1 fi +echo " +apisix: + enable_admin: true + stream_proxy: + tcp: + - addr: 9100 +" > conf/config.yaml + +make init + +count=$(grep -c "lua_package_path" conf/nginx.conf) +if [ "$count" -ne 2 ]; then + echo "failed: failed to enable stream proxy and http proxy when admin is enabled" + exit 1 +fi + echo "passed: enable stream proxy and http proxy" echo " From 5be439094bb1ab19056af394b17a4846bffc8d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 23 Dec 2021 14:57:25 +0800 Subject: [PATCH 215/260] test: make t/plugin/serverless.t stable (#5889) --- t/plugin/serverless.t | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/t/plugin/serverless.t b/t/plugin/serverless.t index 7a1b01befef4..3c8163c1ed41 100644 --- a/t/plugin/serverless.t +++ b/t/plugin/serverless.t @@ -698,7 +698,7 @@ match uri /hello }, "upstream": { "nodes": { - "127.0.0.2:1979": 100000, + "0.0.0.0:1979": 100000, "127.0.0.1:1980": 1 }, "type": "chash", @@ -730,7 +730,7 @@ GET /log_request --- grep_error_log eval qr/(proxy request to \S+|x-serverless: [\d.]+)/ --- grep_error_log_out -proxy request to 127.0.0.2:1979 +proxy request to 0.0.0.0:1979 proxy request to 127.0.0.1:1980 x-serverless: 127.0.0.1 @@ -752,7 +752,7 @@ x-serverless: 127.0.0.1 }, "upstream": { "nodes": { - "127.0.0.2:1979": 100000, + "0.0.0.0:1979": 100000, "127.0.0.1:1980": 1 }, "type": "chash", @@ -802,7 +802,7 @@ GET /log_request }, "upstream": { "nodes": { - "127.0.0.2:1979": 100000, + "0.0.0.0:1979": 100000, "127.0.0.1:1980": 1 }, "type": "chash", @@ -833,5 +833,5 @@ GET /log_request --- grep_error_log eval qr/(run balancer phase with [\d.]+)/ --- grep_error_log_out -run balancer phase with 127.0.0.2 +run balancer phase with 0.0.0.0 run balancer phase with 127.0.0.1 From 79601b820f15ddb98c85237e8a7b45f3263fb5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Thu, 23 Dec 2021 14:57:46 +0800 Subject: [PATCH 216/260] chore(google-cloud-logging): use batch queue default parameters (#5869) --- apisix/plugins/google-cloud-logging.lua | 10 --- .../en/latest/plugins/google-cloud-logging.md | 4 +- .../zh/latest/plugins/google-cloud-logging.md | 4 +- t/plugin/google-cloud-logging2.t | 90 +++++++++++++++++++ 4 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 t/plugin/google-cloud-logging2.t diff --git a/apisix/plugins/google-cloud-logging.lua b/apisix/plugins/google-cloud-logging.lua index a28e7f62d463..7091501f39a3 100644 --- a/apisix/plugins/google-cloud-logging.lua +++ b/apisix/plugins/google-cloud-logging.lua @@ -87,16 +87,6 @@ local schema = { type = "string", default = "apisix.apache.org%2Flogs" }, - inactive_timeout = { - type = "integer", - minimum = 1, - default = 10 - }, - batch_max_size = { - type = "integer", - minimum = 1, - default = 100 - }, }, oneOf = { { required = { "auth_config" } }, diff --git a/docs/en/latest/plugins/google-cloud-logging.md b/docs/en/latest/plugins/google-cloud-logging.md index 5fe10d5d8ef9..d316beb2cb57 100644 --- a/docs/en/latest/plugins/google-cloud-logging.md +++ b/docs/en/latest/plugins/google-cloud-logging.md @@ -55,8 +55,8 @@ For more info on Batch-Processor in Apache APISIX please refer: | max_retry_count | optional | 0 | max number of retries before removing from the processing pipe line | | retry_delay | optional | 1 | number of seconds the process execution should be delayed if the execution fails | | buffer_duration | optional | 60 | max age in seconds of the oldest entry in a batch before the batch must be processed | -| inactive_timeout | optional | 10 | max age in seconds when the buffer will be flushed if inactive | -| batch_max_size | optional | 100 | max size of each batch | +| inactive_timeout | optional | 5 | max age in seconds when the buffer will be flushed if inactive | +| batch_max_size | optional | 1000 | max size of each batch | ## How To Enable diff --git a/docs/zh/latest/plugins/google-cloud-logging.md b/docs/zh/latest/plugins/google-cloud-logging.md index b094b7b066d9..404e9f7a19c3 100644 --- a/docs/zh/latest/plugins/google-cloud-logging.md +++ b/docs/zh/latest/plugins/google-cloud-logging.md @@ -55,8 +55,8 @@ title: google-cloud-logging | max_retry_count | 可选 | 0 | 从处理管道中移除之前的最大重试次数 | | retry_delay | 可选 | 1 | 如果执行失败,流程执行应延迟的秒数 | | buffer_duration | 可选 | 60 | 必须先处理批次中最旧条目的最大期限(以秒为单位) | -| inactive_timeout | 可选 | 10 | 刷新缓冲区的最大时间(以秒为单位) | -| batch_max_size | 可选 | 100 | 每个批处理队列可容纳的最大条目数 | +| inactive_timeout | 可选 | 5 | 刷新缓冲区的最大时间(以秒为单位) | +| batch_max_size | 可选 | 1000 | 每个批处理队列可容纳的最大条目数 | ## 如何开启 diff --git a/t/plugin/google-cloud-logging2.t b/t/plugin/google-cloud-logging2.t new file mode 100644 index 000000000000..4b52ebc6a8ea --- /dev/null +++ b/t/plugin/google-cloud-logging2.t @@ -0,0 +1,90 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + +}); + +run_tests(); + +__DATA__ + +=== TEST 1: set route (verify batch queue default params) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/hello", + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + }, + plugins = { + ["google-cloud-logging"] = { + auth_file = "t/plugin/google-cloud-logging/config.json", + } + } + } + + local expected = { + node = { + value = { + plugins = { + ["google-cloud-logging"] = { + max_retry_count = 0, + retry_delay = 1, + buffer_duration = 60, + batch_max_size = 1000, + inactive_timeout = 5, + } + } + } + } + } + + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config, expected) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed From 3c0c89a58ee872c9564ded3fe6e8998f96235489 Mon Sep 17 00:00:00 2001 From: jackfu Date: Thu, 23 Dec 2021 19:12:16 +0800 Subject: [PATCH 217/260] fix(cors): compatible with scenarios where origin is modified (#5890) Co-authored-by: jack.fu --- apisix/plugins/cors.lua | 4 ++- t/plugin/cors.t | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/cors.lua b/apisix/plugins/cors.lua index 0c6f901209a8..63751732f35f 100644 --- a/apisix/plugins/cors.lua +++ b/apisix/plugins/cors.lua @@ -227,6 +227,8 @@ end function _M.rewrite(conf, ctx) + -- save the original request origin as it may be changed at other phase + ctx.original_request_origin = core.request.header(ctx, "Origin") if ctx.var.request_method == "OPTIONS" then return 200 end @@ -234,7 +236,7 @@ end function _M.header_filter(conf, ctx) - local req_origin = core.request.header(ctx, "Origin") + local req_origin = ctx.original_request_origin -- Try allow_origins first, if mismatched, try allow_origins_by_regex. local allow_origins allow_origins = process_with_allow_origins(conf, ctx, req_origin) diff --git a/t/plugin/cors.t b/t/plugin/cors.t index eef790d03ee1..072dee165d99 100644 --- a/t/plugin/cors.t +++ b/t/plugin/cors.t @@ -927,3 +927,72 @@ Access-Control-Max-Age: Access-Control-Allow-Credentials: --- no_error_log [error] + + + +=== TEST 34: origin was modified by the proxy_rewrite plugin +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "cors": { + "allow_origins": "http://sub.domain.com", + "allow_methods": "GET,POST", + "allow_headers": "headr1,headr2", + "expose_headers": "ex-headr1,ex-headr2", + "max_age": 50, + "allow_credential": true + }, + "proxy-rewrite": { + "headers": { + "Origin": "http://example.com" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 35: origin is not affected by proxy_rewrite plugins +--- request +GET /hello HTTP/1.1 +--- more_headers +Origin: http://sub.domain.com +resp-vary: Via +--- response_body +hello world +--- response_headers +Access-Control-Allow-Origin: http://sub.domain.com +Vary: Via, Origin +Access-Control-Allow-Methods: GET,POST +Access-Control-Allow-Headers: headr1,headr2 +Access-Control-Expose-Headers: ex-headr1,ex-headr2 +Access-Control-Max-Age: 50 +Access-Control-Allow-Credentials: true +--- no_error_log +[error] From abbe266d35c7ce4e0173bf1b62b2175333187b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 24 Dec 2021 15:49:51 +0800 Subject: [PATCH 218/260] ci: the trailing whitespace check is already covered by editconfig (#5903) --- .github/workflows/lint.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9b9ac51e58aa..9ab6bf41f00f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,14 +3,6 @@ name: ❄️ Lint on: [pull_request] jobs: - trailing-whitespace: - name: 🌌 Trailing whitespace - runs-on: ubuntu-latest - timeout-minutes: 1 - steps: - - uses: actions/checkout@v2.4.0 - - name: 🧹 Check for trailing whitespace - run: "! git grep -EIn $'[ \t]+$'" misc: name: misc checker runs-on: ubuntu-latest From 7a56580a22e6e4287b5c463de5beef165ed22e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 24 Dec 2021 15:50:03 +0800 Subject: [PATCH 219/260] feat(limit-count): allow sharing counter (#5881) Co-authored-by: leslie <59061168+leslie-tsang@users.noreply.github.com> Co-authored-by: tzssangglass --- apisix/plugins/limit-count.lua | 139 +++++++++---- docs/en/latest/plugins/limit-count.md | 49 +++++ docs/zh/latest/plugins/limit-count.md | 49 +++++ t/plugin/limit-count-redis2.t | 116 ++++++++--- t/plugin/limit-count2.t | 269 ++++++++++++++++++++++---- 5 files changed, 525 insertions(+), 97 deletions(-) diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua index cbce3a798c6c..5b0e8c37c9f4 100644 --- a/apisix/plugins/limit-count.lua +++ b/apisix/plugins/limit-count.lua @@ -16,6 +16,11 @@ -- local limit_local_new = require("resty.limit.count").new local core = require("apisix.core") +local tab_insert = table.insert +local ipairs = ipairs +local pairs = pairs + + local plugin_name = "limit-count" local limit_redis_cluster_new local limit_redis_new @@ -29,13 +34,60 @@ end local lrucache = core.lrucache.new({ type = 'plugin', serial_creating = true, }) +local group_conf_lru = core.lrucache.new({ + type = 'plugin', +}) +local policy_to_additional_properties = { + redis = { + properties = { + redis_host = { + type = "string", minLength = 2 + }, + redis_port = { + type = "integer", minimum = 1, default = 6379, + }, + redis_password = { + type = "string", minLength = 0, + }, + redis_database = { + type = "integer", minimum = 0, default = 0, + }, + redis_timeout = { + type = "integer", minimum = 1, default = 1000, + }, + }, + required = {"redis_host"}, + }, + ["redis-cluster"] = { + properties = { + redis_cluster_nodes = { + type = "array", + minItems = 2, + items = { + type = "string", minLength = 2, maxLength = 100 + }, + }, + redis_password = { + type = "string", minLength = 0, + }, + redis_timeout = { + type = "integer", minimum = 1, default = 1000, + }, + redis_cluster_name = { + type = "string", + }, + }, + required = {"redis_cluster_nodes", "redis_cluster_name"}, + }, +} local schema = { type = "object", properties = { count = {type = "integer", exclusiveMinimum = 0}, time_window = {type = "integer", exclusiveMinimum = 0}, + group = {type = "string"}, key = {type = "string", default = "remote_addr"}, key_type = {type = "string", enum = {"var", "var_combination"}, @@ -66,53 +118,20 @@ local schema = { }, }, }, - { + core.table.merge({ properties = { policy = { enum = {"redis"}, }, - redis_host = { - type = "string", minLength = 2 - }, - redis_port = { - type = "integer", minimum = 1, default = 6379, - }, - redis_password = { - type = "string", minLength = 0, - }, - redis_database = { - type = "integer", minimum = 0, default = 0, - }, - redis_timeout = { - type = "integer", minimum = 1, default = 1000, - }, }, - required = {"redis_host"}, - }, - { + }, policy_to_additional_properties.redis), + core.table.merge({ properties = { policy = { enum = {"redis-cluster"}, }, - redis_cluster_nodes = { - type = "array", - minItems = 2, - items = { - type = "string", minLength = 2, maxLength = 100 - }, - }, - redis_password = { - type = "string", minLength = 0, - }, - redis_timeout = { - type = "integer", minimum = 1, default = 1000, - }, - redis_cluster_name = { - type = "string", - }, }, - required = {"redis_cluster_nodes", "redis_cluster_name"}, - } + }, policy_to_additional_properties["redis-cluster"]), } } } @@ -127,12 +146,42 @@ local _M = { } +local function group_conf(conf) + return conf +end + + function _M.check_schema(conf) local ok, err = core.schema.check(schema, conf) if not ok then return false, err end + if conf.group then + local fields = {} + for k in pairs(schema.properties) do + tab_insert(fields, k) + end + local extra = policy_to_additional_properties[conf.policy] + if extra then + for k in pairs(extra.properties) do + tab_insert(fields, k) + end + end + + local prev_conf = group_conf_lru(conf.group, "", group_conf, conf) + + for _, field in ipairs(fields) do + if not core.table.deep_eq(prev_conf[field], conf[field]) then + core.log.error("previous limit-conn group ", prev_conf.group, + " conf: ", core.json.encode(prev_conf)) + core.log.error("current limit-conn group ", conf.group, + " conf: ", core.json.encode(conf)) + return false, "group conf mismatched" + end + end + end + return true end @@ -161,7 +210,14 @@ end function _M.access(conf, ctx) core.log.info("ver: ", ctx.conf_version) - local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, conf.policy, create_limit_obj, conf) + + local lim, err + if not conf.group then + lim, err = core.lrucache.plugin_ctx(lrucache, ctx, conf.policy, create_limit_obj, conf) + else + lim, err = lrucache(conf.group, "", create_limit_obj, conf) + end + if not lim then core.log.error("failed to fetch limit.count object: ", err) if conf.allow_degradation then @@ -192,7 +248,12 @@ function _M.access(conf, ctx) key = ctx.var["remote_addr"] end - key = key .. ctx.conf_type .. ctx.conf_version + if not conf.group then + key = key .. ctx.conf_type .. ctx.conf_version + else + key = key .. conf.group + end + core.log.info("limit key: ", key) local delay, remaining = lim:incoming(key, true) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 5e094d540dba..0062596b3912 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -46,6 +46,7 @@ Limit request rate by a fixed number of requests in a given time window. | policy | string | optional | "local" | ["local", "redis", "redis-cluster"] | The rate-limiting policies to use for retrieving and incrementing the limits. Available values are `local`(the counters will be stored locally in-memory on the node), `redis`(counters are stored on a Redis server and will be shared across the nodes, usually use it to do the global speed limit), and `redis-cluster` which works the same as `redis` but with redis cluster. | | allow_degradation | boolean | optional | false | | Whether to enable plugin degradation when the limit-count function is temporarily unavailable(e.g. redis timeout). Allow requests to continue when the value is set to true, default false. | | show_limit_quota_header | boolean | optional | true | | Whether show `X-RateLimit-Limit` and `X-RateLimit-Remaining` (which mean the total number of requests and the remaining number of requests that can be sent) in the response header, default true. | +| group | string | optional | | non-empty | Route configured with the same group will share the same counter | | redis_host | string | required for `redis` | | | When using the `redis` policy, this property specifies the address of the Redis server. | | redis_port | integer | optional | 6379 | [1,...] | When using the `redis` policy, this property specifies the port of the Redis server. | | redis_password | string | optional | | | When using the `redis` or `redis-cluster` policy, this property specifies the password of the Redis server. | @@ -107,6 +108,54 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 You also can complete the above operation through the web interface, first add a route, then add limit-count plugin: ![Add limit-count plugin.](../../../assets/images/plugin/limit-count-1.png) +It is possible to share the same limit counter across different routes. For example, + +``` +curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "limit-count": { + "count": 1, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "group": "services_1#1640140620" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +Every route which group name is "services_1#1640140620" will share the same count limitation `1` in one minute per remote_addr. + +``` +$ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "service_id": "1", + "uri": "/hello" +}' + +$ curl -i http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "service_id": "1", + "uri": "/hello2" +}' + +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 ... + +$ curl -i http://127.0.0.1:9080/hello2 +HTTP/1.1 503 ... +``` + +Note that every limit-count configuration of the same group must be the same. +Therefore, once update the configuration, we also need to update the group name. + If you need a cluster-level precision traffic limit, then we can do it with the redis server. The rate limit of the traffic will be shared between different APISIX nodes to limit the rate of cluster traffic. Here is the example if we use single `redis` policy: diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index a3e9c7aad6c1..4d7273d3e1e6 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -49,6 +49,7 @@ title: limit-count | policy | string | 可选 | "local" | ["local", "redis", "redis-cluster"] | 用于检索和增加限制的速率限制策略。可选的值有:`local`(计数器被以内存方式保存在节点本地,默认选项) 和 `redis`(计数器保存在 Redis 服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速);以及`redis-cluster`,跟 redis 功能一样,只是使用 redis 集群方式。 | | allow_degradation | boolean | 可选 | false | | 当限流插件功能临时不可用时(例如,Redis 超时)是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| | show_limit_quota_header | boolean | 可选 | true | | 是否在响应头中显示 `X-RateLimit-Limit` 和 `X-RateLimit-Remaining` (限制的总请求数和剩余还可以发送的请求数),默认值是 true。 | +| group | string | 可选 | | 非空 | 配置同样的 group 的 Route 将共享同样的限流计数器 | | redis_host | string | `redis` 必须 | | | 当使用 `redis` 限速策略时,该属性是 Redis 服务节点的地址。 | | redis_port | integer | 可选 | 6379 | [1,...] | 当使用 `redis` 限速策略时,该属性是 Redis 服务节点的端口 | | redis_password | string | 可选 | | | 当使用 `redis` 或者 `redis-cluster` 限速策略时,该属性是 Redis 服务节点的密码。 | @@ -112,6 +113,54 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 你也可以通过 web 界面来完成上面的操作,先增加一个 route,然后在插件页面中添加 limit-count 插件: ![添加插件](../../../assets/images/plugin/limit-count-1.png) +我们也支持在多个 Route 间共享同一个限流计数器。举个例子, + +``` +curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "limit-count": { + "count": 1, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "group": "services_1#1640140620" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +每个配置了 `group` 为 `services_1#1640140620` 的 Route 都将共享同一个每个 IP 地址每分钟只能访问一次的计数器。 + +``` +$ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "service_id": "1", + "uri": "/hello" +}' + +$ curl -i http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "service_id": "1", + "uri": "/hello2" +}' + +$ curl -i http://127.0.0.1:9080/hello +HTTP/1.1 200 ... + +$ curl -i http://127.0.0.1:9080/hello2 +HTTP/1.1 503 ... +``` + +注意同一个 group 里面的 limit-count 配置必须一样。 +所以,一旦修改了配置,我们需要更新对应的 group 的值。 + 如果你需要一个集群级别的流量控制,我们可以借助 redis server 来完成。不同的 APISIX 节点之间将共享流量限速结果,实现集群流量限速。 如果启用单 redis 策略,请看下面例子: diff --git a/t/plugin/limit-count-redis2.t b/t/plugin/limit-count-redis2.t index 5be2efe6ec51..a9f23e858f7b 100644 --- a/t/plugin/limit-count-redis2.t +++ b/t/plugin/limit-count-redis2.t @@ -30,6 +30,19 @@ repeat_each(1); no_long_string(); no_shuffle(); no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + run_tests; __DATA__ @@ -70,12 +83,8 @@ __DATA__ ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -116,12 +125,8 @@ passed ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -172,12 +177,8 @@ failed to limit count: failed to change redis db, err: ERR invalid DB index ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -186,8 +187,6 @@ passed ["GET /hello", "GET /hello", "GET /hello", "GET /hello"] --- error_code eval [200, 200, 503, 503] ---- no_error_log -[error] @@ -229,12 +228,8 @@ passed ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -243,6 +238,8 @@ passed GET /hello --- response_body hello world +--- error_log +connection refused @@ -284,12 +281,8 @@ hello world ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -298,3 +291,82 @@ passed GET /hello --- raw_response_headers_unlike eval qr/X-RateLimit-Limit/ + + + +=== TEST 10: configuration from the same group should be the same +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "show_limit_quota_header": false, + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_database": 1, + "redis_timeout": 1001, + "group": "redis" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "show_limit_quota_header": false, + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_database": 2, + "redis_timeout": 1001, + "group": "redis" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- error_log +[error] +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: group conf mismatched"} diff --git a/t/plugin/limit-count2.t b/t/plugin/limit-count2.t index ea4a675c2db9..3f6bdf325b58 100644 --- a/t/plugin/limit-count2.t +++ b/t/plugin/limit-count2.t @@ -214,12 +214,8 @@ GET /hello ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -244,10 +240,6 @@ passed ngx.say(json.encode(ress)) } } ---- request -GET /t ---- no_error_log -[error] --- response_body [200,200,503,503] @@ -274,10 +266,6 @@ GET /t ngx.say(json.encode(ress)) } } ---- request -GET /t ---- no_error_log -[error] --- response_body [200,200,503,503] @@ -316,12 +304,8 @@ GET /t ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -346,10 +330,6 @@ passed ngx.say(json.encode(ress)) } } ---- request -GET /t ---- no_error_log -[error] --- response_body [200,200,503,503] @@ -376,10 +356,6 @@ GET /t ngx.say(json.encode(ress)) } } ---- request -GET /t ---- no_error_log -[error] --- response_body [503,200] @@ -406,10 +382,6 @@ GET /t ngx.say(json.encode(ress)) } } ---- request -GET /t ---- no_error_log -[error] --- response_body [200,200,503,503] --- error_log @@ -450,12 +422,8 @@ The value of the configured key is empty, use client IP instead ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] @@ -480,11 +448,240 @@ passed ngx.say(json.encode(ress)) } } ---- request -GET /t ---- no_error_log -[error] --- response_body [200,200,503,503] --- error_log The value of the configured key is empty, use client IP instead + + + +=== TEST 15: limit count in group +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "group": "services_1" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + return + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "group": "services_1" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello_chunked" + }]] + ) + if code >= 300 then + ngx.status = code + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 16: hit multiple paths +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri1 = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local uri2 = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello_chunked" + local ress = {} + for i = 1, 4 do + local httpc = http.new() + local uri + if i % 2 == 1 then + uri = uri1 + else + uri = uri2 + end + + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- response_body +[200,200,503,503] + + + +=== TEST 17: limit count in group, configuration is from services +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "group": "afafafhao" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + return + end + + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "service_id": "1", + "uri": "/hello" + }]] + ) + if code >= 300 then + ngx.status = code + return + end + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "service_id": "1", + "uri": "/hello_chunked" + }]] + ) + if code >= 300 then + ngx.status = code + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 18: hit multiple paths +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri1 = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local uri2 = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello_chunked" + local ress = {} + for i = 1, 4 do + local httpc = http.new() + local uri + if i % 2 == 1 then + uri = uri1 + else + uri = uri2 + end + + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- response_body +[200,200,503,503] + + + +=== TEST 19: configuration from the same group should be the same +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 1, + "time_window": 60, + "rejected_code": 503, + "group": "afafafhao" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } + } +--- error_code: 400 +--- error_log +[error] +--- response_body +{"error_msg":"failed to check the configuration of plugin limit-count err: group conf mismatched"} From 9a1831d656b527fcb07ce5e556da0ccc502300bc Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:02:38 +0800 Subject: [PATCH 220/260] fix(proxy-rewrite): make sure proxy-rewrite update the core.request.header cache (#5914) --- apisix/plugins/proxy-rewrite.lua | 4 ++-- t/plugin/proxy-rewrite2.t | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/proxy-rewrite.lua b/apisix/plugins/proxy-rewrite.lua index 0780b211dab6..eff3c2a256b9 100644 --- a/apisix/plugins/proxy-rewrite.lua +++ b/apisix/plugins/proxy-rewrite.lua @@ -209,8 +209,8 @@ function _M.rewrite(conf, ctx) local field_cnt = #conf.headers_arr for i = 1, field_cnt, 2 do - ngx.req.set_header(conf.headers_arr[i], - core.utils.resolve_var(conf.headers_arr[i+1], ctx.var)) + core.request.set_header(ctx, conf.headers_arr[i], + core.utils.resolve_var(conf.headers_arr[i+1], ctx.var)) end if conf.method then diff --git a/t/plugin/proxy-rewrite2.t b/t/plugin/proxy-rewrite2.t index e3d1b7b157af..4fbfe55bab34 100644 --- a/t/plugin/proxy-rewrite2.t +++ b/t/plugin/proxy-rewrite2.t @@ -175,3 +175,36 @@ GET /echo X-Forwarded-Proto: grpc --- response_headers X-Forwarded-Proto: https + + + +=== TEST 6: make sure X-Forwarded-Proto hit the `core.request.header` cache +--- apisix_yaml +routes: + - + id: 1 + uri: /echo + plugins: + serverless-pre-function: + phase: rewrite + functions: + - return function(conf, ctx) local core = require("apisix.core"); ngx.log(ngx.ERR, core.request.header(ctx, "host")); end + proxy-rewrite: + headers: + X-Forwarded-Proto: https-rewrite + upstream_id: 1 +upstreams: + - + id: 1 + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +--- request +GET /echo +--- more_headers +X-Forwarded-Proto: grpc +--- response_headers +X-Forwarded-Proto: https-rewrite +--- error_log +localhost From 50af9bc9dcfd931568f07505972dcba42e5661ca Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Sun, 26 Dec 2021 19:16:26 +0800 Subject: [PATCH 221/260] chore: remove install-etcd.sh as it is no longer being used (#5932) --- utils/install-etcd.sh | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100755 utils/install-etcd.sh diff --git a/utils/install-etcd.sh b/utils/install-etcd.sh deleted file mode 100755 index 83b3d153758d..000000000000 --- a/utils/install-etcd.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -# -# 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. -# - -wget https://github.com/etcd-io/etcd/releases/download/v3.4.0/etcd-v3.4.0-linux-amd64.tar.gz -tar xf etcd-v3.4.0-linux-amd64.tar.gz -sudo cp etcd-v3.4.0-linux-amd64/etcd /usr/local/bin/ -sudo cp etcd-v3.4.0-linux-amd64/etcdctl /usr/local/bin/ -rm -rf etcd-v3.4.0-linux-amd64 From db5813ff9ac2e77960af6c6ac3072eccee2057c7 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 27 Dec 2021 15:42:40 +0800 Subject: [PATCH 222/260] fix(logger): remove incorrect item type of include req/resp body expr (#5886) --- apisix/plugins/http-logger.lua | 5 +- apisix/plugins/kafka-logger.lua | 10 +-- apisix/plugins/rocketmq-logger.lua | 10 +-- t/plugin/http-logger-log-format.t | 28 ++++++++ t/plugin/kafka-logger2.t | 106 +++++++++++++++++++++++++++++ t/plugin/rocketmq-logger2.t | 33 +++++++++ 6 files changed, 172 insertions(+), 20 deletions(-) diff --git a/apisix/plugins/http-logger.lua b/apisix/plugins/http-logger.lua index d796eea2e08c..e4e403c66271 100644 --- a/apisix/plugins/http-logger.lua +++ b/apisix/plugins/http-logger.lua @@ -41,10 +41,7 @@ local schema = { type = "array", minItems = 1, items = { - type = "array", - items = { - type = "string" - } + type = "array" } }, concat_method = {type = "string", default = "json", diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua index 5b6e90380881..2947d145e468 100644 --- a/apisix/plugins/kafka-logger.lua +++ b/apisix/plugins/kafka-logger.lua @@ -69,10 +69,7 @@ local schema = { type = "array", minItems = 1, items = { - type = "array", - items = { - type = "string" - } + type = "array" } }, include_resp_body = {type = "boolean", default = false}, @@ -80,10 +77,7 @@ local schema = { type = "array", minItems = 1, items = { - type = "array", - items = { - type = "string" - } + type = "array" } }, -- in lua-resty-kafka, cluster_name is defined as number diff --git a/apisix/plugins/rocketmq-logger.lua b/apisix/plugins/rocketmq-logger.lua index 247c84888bdd..447960ba689c 100644 --- a/apisix/plugins/rocketmq-logger.lua +++ b/apisix/plugins/rocketmq-logger.lua @@ -57,10 +57,7 @@ local schema = { type = "array", minItems = 1, items = { - type = "array", - items = { - type = "string" - } + type = "array" } }, include_resp_body = {type = "boolean", default = false}, @@ -68,10 +65,7 @@ local schema = { type = "array", minItems = 1, items = { - type = "array", - items = { - type = "string" - } + type = "array" } }, }, diff --git a/t/plugin/http-logger-log-format.t b/t/plugin/http-logger-log-format.t index 07978f5c197d..a9d05b27d18f 100644 --- a/t/plugin/http-logger-log-format.t +++ b/t/plugin/http-logger-log-format.t @@ -433,3 +433,31 @@ qr/request log: \{.+\}/ --- grep_error_log_out eval qr/\Q{"client_ip":"127.0.0.1","consumer":{"username":"jack"},"latency":\E[^,]+\Q,"request":{"headers":{"apikey":"auth-one","connection":"close","host":"localhost"},"method":"GET","querystring":{},"size":\E\d+\Q,"uri":"\/hello","url":"http:\/\/localhost:1984\/hello"},"response":{"headers":{"connection":"close","content-length":"\E\d+\Q","content-type":"text\/plain","server":"\E[^"]+\Q"},"size":\E\d+\Q,"status":200},"route_id":"1","server":{"hostname":"\E[^"]+\Q","version":"\E[^"]+\Q"},"service_id":"","start_time":\E\d+\Q,"upstream":"127.0.0.1:1982"}\E/ --- wait: 0.5 + + + +=== TEST 14: multi level nested expr conditions +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.http-logger") + local ok, err = plugin.check_schema({ + uri = "http://127.0.0.1", + include_resp_body = true, + include_resp_body_expr = { + {"http_content_length", "<", 1024}, + {"http_content_type", "in", {"application/xml", "application/json", "text/plain", "text/xml"}} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] diff --git a/t/plugin/kafka-logger2.t b/t/plugin/kafka-logger2.t index 73ffec5242e2..c78da6f59041 100644 --- a/t/plugin/kafka-logger2.t +++ b/t/plugin/kafka-logger2.t @@ -609,3 +609,109 @@ hello world --- no_error_log eval qr/send data to kafka: \{.*"body":"hello world\\n"/ --- wait: 2 + + + +=== TEST 15: multi level nested expr conditions +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin").test + local kafka = { + kafka_topic = "test2", + key = "key1", + batch_max_size = 1, + broker_list = { + ["127.0.0.1"] = 9092 + }, + timeout = 3, + include_req_body = true, + include_req_body_expr = { + {"request_length", "<", 1054}, + {"arg_name", "in", {"qwerty", "asdfgh"}} + }, + include_resp_body = true, + include_resp_body_expr = { + {"http_content_length", "<", 1054}, + {"arg_name", "in", {"qwerty", "zxcvbn"}} + } + } + local plugins = {} + plugins["kafka-logger"] = kafka + local data = { + plugins = plugins + } + data.upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 1 + } + } + data.uri = "/hello" + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + core.json.encode(data) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 16: hit route, req_body_expr and resp_body_expr both eval success +--- request +POST /hello?name=qwerty +abcdef +--- response_body +hello world +--- error_log eval +[qr/send data to kafka: \{.*"body":"abcdef"/, +qr/send data to kafka: \{.*"body":"hello world\\n"/] +--- wait: 2 + + + +=== TEST 17: hit route, req_body_expr eval success, resp_body_expr both eval failed +--- request +POST /hello?name=asdfgh +abcdef +--- response_body +hello world +--- error_log eval +qr/send data to kafka: \{.*"body":"abcdef"/ +--- no_error_log eval +qr/send data to kafka: \{.*"body":"hello world\\n"/ +--- wait: 2 + + + +=== TEST 18: hit route, req_body_expr eval failed, resp_body_expr both eval success +--- request +POST /hello?name=zxcvbn +abcdef +--- response_body +hello world +--- error_log eval +qr/send data to kafka: \{.*"body":"hello world\\n"/ +--- no_error_log eval +qr/send data to kafka: \{.*"body":"abcdef"/ +--- wait: 2 + + + +=== TEST 19: hit route, req_body_expr eval success, resp_body_expr both eval failed +--- request +POST /hello?name=xxxxxx +abcdef +--- response_body +hello world +--- no_error_log eval +[qr/send data to kafka: \{.*"body":"abcdef"/, +qr/send data to kafka: \{.*"body":"hello world\\n"/] +--- wait: 2 diff --git a/t/plugin/rocketmq-logger2.t b/t/plugin/rocketmq-logger2.t index fcc378b70269..286d3cad4fe0 100644 --- a/t/plugin/rocketmq-logger2.t +++ b/t/plugin/rocketmq-logger2.t @@ -412,3 +412,36 @@ hello world --- no_error_log eval qr/send data to rocketmq: \{.*"body":"hello world\\n"/ --- wait: 2 + + + +=== TEST 13: multi level nested expr conditions +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.rocketmq-logger") + local ok, err = plugin.check_schema({ + topic = "test", + key = "key1", + nameserver_list = { + "127.0.0.1:3" + }, + include_req_body = true, + include_req_body_expr = { + {"request_length", "<", 1024}, + {"http_content_type", "in", {"application/xml", "application/json", "text/plain", "text/xml"}} + }, + include_resp_body = true, + include_resp_body_expr = { + {"http_content_length", "<", 1024}, + {"http_content_type", "in", {"application/xml", "application/json", "text/plain", "text/xml"}} + } + }) + if not ok then + ngx.say(err) + end + ngx.say("done") + } + } +--- response_body +done From ae2e653f6b354f2fbb34650c2d3a3e5a69f5a5d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 27 Dec 2021 21:01:59 +0800 Subject: [PATCH 223/260] fix(mqtt): handle properties for MQTT 5 (#5916) --- apisix/stream/plugins/mqtt-proxy.lua | 83 ++++++++++++++++------------ t/stream-plugin/mqtt-proxy.t | 73 +++++++++++++++++++++++- 2 files changed, 121 insertions(+), 35 deletions(-) diff --git a/apisix/stream/plugins/mqtt-proxy.lua b/apisix/stream/plugins/mqtt-proxy.lua index fae0eb08f5f4..7c890505aa9b 100644 --- a/apisix/stream/plugins/mqtt-proxy.lua +++ b/apisix/stream/plugins/mqtt-proxy.lua @@ -67,27 +67,39 @@ function _M.check_schema(conf) end -local function parse_mqtt(data) - local res = {} - res.packet_type_flags_byte = str_byte(data, 1, 1) - if res.packet_type_flags_byte < 16 or res.packet_type_flags_byte > 32 then - return nil, "Received unexpected MQTT packet type+flags: " - .. res.packet_type_flags_byte - end - - local parsed_pos = 1 - res.remaining_len = 0 +local function decode_variable_byte_int(data, offset) local multiplier = 1 - for i = 2, 5 do - parsed_pos = i + local len = 0 + local pos + for i = offset, offset + 3 do + pos = i local byte = str_byte(data, i, i) - res.remaining_len = res.remaining_len + bit.band(byte, 127) * multiplier + len = len + bit.band(byte, 127) * multiplier multiplier = multiplier * 128 if bit.band(byte, 128) == 0 then break end end + return len, pos +end + + +local function parse_msg_hdr(data) + local packet_type_flags_byte = str_byte(data, 1, 1) + if packet_type_flags_byte < 16 or packet_type_flags_byte > 32 then + return nil, nil, + "Received unexpected MQTT packet type+flags: " .. packet_type_flags_byte + end + + local len, pos = decode_variable_byte_int(data, 2) + return len, pos +end + + +local function parse_mqtt(data, parsed_pos) + local res = {} + local protocol_len = str_byte(data, parsed_pos + 1, parsed_pos + 1) * 256 + str_byte(data, parsed_pos + 2, parsed_pos + 2) parsed_pos = parsed_pos + 2 @@ -96,10 +108,15 @@ local function parse_mqtt(data) res.protocol_ver = str_byte(data, parsed_pos + 1, parsed_pos + 1) parsed_pos = parsed_pos + 1 - if res.protocol_ver == 4 then - parsed_pos = parsed_pos + 3 - elseif res.protocol_ver == 5 then - parsed_pos = parsed_pos + 9 + + -- skip control flags & keepalive + parsed_pos = parsed_pos + 3 + + if res.protocol_ver == 5 then + -- skip properties + local property_len + property_len, parsed_pos = decode_variable_byte_int(data, parsed_pos + 1) + parsed_pos = parsed_pos + property_len end local client_id_len = str_byte(data, parsed_pos + 1, parsed_pos + 1) * 256 @@ -129,31 +146,29 @@ function _M.preread(conf, ctx) local sock = ngx.req.socket() -- the header format of MQTT CONNECT can be found in -- https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901033 - local data, err = sock:peek(14) + local data, err = sock:peek(5) if not data then - core.log.error("failed to read first 16 bytes: ", err) + core.log.error("failed to read the msg header: ", err) return 503 end - local res, err = parse_mqtt(data) - if not res then - core.log.error("failed to parse the first 16 bytes: ", err) + local remain_len, pos, err = parse_msg_hdr(data) + if not remain_len then + core.log.error("failed to parse the msg header: ", err) return 503 end - if res.expect_len > #data then - data, err = sock:peek(res.expect_len) - if not data then - core.log.error("failed to read ", res.expect_len, " bytes: ", err) - return 503 - end + local data, err = sock:peek(pos + remain_len) + if not data then + core.log.error("failed to read the Connect Command: ", err) + return 503 + end - res = parse_mqtt(data) - if res.expect_len > #data then - core.log.error("failed to parse mqtt request, expect len: ", - res.expect_len, " but got ", #data) - return 503 - end + local res = parse_mqtt(data, pos) + if res.expect_len > #data then + core.log.error("failed to parse mqtt request, expect len: ", + res.expect_len, " but got ", #data) + return 503 end if res.protocol and res.protocol ~= conf.protocol_name then diff --git a/t/stream-plugin/mqtt-proxy.t b/t/stream-plugin/mqtt-proxy.t index ae46fa8cdcc9..3aa5cdfda0f0 100644 --- a/t/stream-plugin/mqtt-proxy.t +++ b/t/stream-plugin/mqtt-proxy.t @@ -328,7 +328,7 @@ mqtt client id: foo === TEST 13: hit route with empty client id --- stream_enable --- stream_request eval -"\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x00" +"\x10\x0c\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x00" --- stream_response hello world --- grep_error_log eval @@ -336,3 +336,74 @@ qr/mqtt client id: \w+/ --- grep_error_log_out --- no_error_log [error] + + + +=== TEST 14: MQTT 5 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/stream_routes/1', + ngx.HTTP_PUT, + [[{ + "remote_addr": "127.0.0.1", + "server_port": 1985, + "plugins": { + "mqtt-proxy": { + "protocol_name": "MQTT", + "protocol_level": 5 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": [{ + "host": "127.0.0.1", + "port": 1995, + "weight": 1 + }] + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 15: hit route with empty property +--- stream_enable +--- stream_request eval +"\x10\x0d\x00\x04\x4d\x51\x54\x54\x05\x02\x00\x3c\x00\x00\x00" +--- stream_response +hello world +--- grep_error_log eval +qr/mqtt client id: \w+/ +--- grep_error_log_out +--- no_error_log +[error] + + + +=== TEST 16: hit route with property +--- stream_enable +--- stream_request eval +"\x10\x1b\x00\x04\x4d\x51\x54\x54\x05\x02\x00\x3c\x05\x11\x00\x00\x0e\x10\x00\x09\x63\x6c\x69\x6e\x74\x2d\x31\x31\x31" +--- stream_response +hello world +--- grep_error_log eval +qr/mqtt client id: \S+/ +--- grep_error_log_out +mqtt client id: clint-111 +--- no_error_log +[error] From 2be323564e37a7d7fe16ae4a1675e085f1867120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 27 Dec 2021 21:32:27 +0800 Subject: [PATCH 224/260] docs: fix the title level of API sections (#5939) --- docs/en/latest/plugin-develop.md | 110 +++++++++++++++---------------- docs/zh/latest/plugin-develop.md | 98 +++++++++++++-------------- 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md index c36f5bc5cccc..cc629e6c7526 100644 --- a/docs/en/latest/plugin-develop.md +++ b/docs/en/latest/plugin-develop.md @@ -32,10 +32,10 @@ title: Plugin Develop - [implement the logic](#implement-the-logic) - [conf parameter](#conf-parameter) - [ctx parameter](#ctx-parameter) +- [Register public API](#register-public-api) +- [Register control API](#register-control-api) - [write test case](#write-test-case) - [Attach the test-nginx execution process:](#attach-the-test-nginx-execution-process) - - [Register public API](#register-public-api) - - [Register control API](#register-control-api) This documentation is about developing plugin in Lua. For other languages, see [external plugin](./external-plugin.md). @@ -388,6 +388,59 @@ function _M.access(conf, ctx) end ``` +## Register public API + +A plugin can register API which exposes to the public. Take jwt-auth plugin as an example, this plugin registers `GET /apisix/plugin/jwt/sign` to allow client to sign its key: + +```lua +local function gen_token() + --... +end + +function _M.api() + return { + { + methods = {"GET"}, + uri = "/apisix/plugin/jwt/sign", + handler = gen_token, + } + } +end +``` + +Note that the public API is exposed to the public. +You may need to use [interceptors](plugin-interceptors.md) to protect it. + +## Register control API + +If you only want to expose the API to the localhost or intranet, you can expose it via [Control API](./control-api.md). + +Take a look at example-plugin plugin: + +```lua +local function hello() + local args = ngx.req.get_uri_args() + if args["json"] then + return 200, {msg = "world"} + else + return 200, "world\n" + end +end + + +function _M.control_api() + return { + { + methods = {"GET"}, + uris = {"/v1/plugin/example-plugin/hello"}, + handler = hello, + } + } +end +``` + +If you don't change the default control API configuration, the plugin will be expose `GET /v1/plugin/example-plugin/hello` which can only be accessed via `127.0.0.1`. + ## write test case For functions, write and improve the test cases of various dimensions, do a comprehensive test for your plugin! The @@ -441,56 +494,3 @@ According to the path we configured in the makefile and some configuration items framework will assemble into a complete nginx.conf file. "__t/servroot__" is the working directory of Nginx and start the Nginx instance. according to the information provided by the test case, initiate the http request and check that the return items of HTTP include HTTP status, HTTP response header, HTTP response body and so on. - -### Register public API - -A plugin can register API which exposes to the public. Take jwt-auth plugin as an example, this plugin registers `GET /apisix/plugin/jwt/sign` to allow client to sign its key: - -```lua -local function gen_token() - --... -end - -function _M.api() - return { - { - methods = {"GET"}, - uri = "/apisix/plugin/jwt/sign", - handler = gen_token, - } - } -end -``` - -Note that the public API is exposed to the public. -You may need to use [interceptors](plugin-interceptors.md) to protect it. - -### Register control API - -If you only want to expose the API to the localhost or intranet, you can expose it via [Control API](./control-api.md). - -Take a look at example-plugin plugin: - -```lua -local function hello() - local args = ngx.req.get_uri_args() - if args["json"] then - return 200, {msg = "world"} - else - return 200, "world\n" - end -end - - -function _M.control_api() - return { - { - methods = {"GET"}, - uris = {"/v1/plugin/example-plugin/hello"}, - handler = hello, - } - } -end -``` - -If you don't change the default control API configuration, the plugin will be expose `GET /v1/plugin/example-plugin/hello` which can only be accessed via `127.0.0.1`. diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md index e8b5b329a7ca..62ddc1199c18 100644 --- a/docs/zh/latest/plugin-develop.md +++ b/docs/zh/latest/plugin-develop.md @@ -31,10 +31,10 @@ title: 插件开发 - [编写执行逻辑](#编写执行逻辑) - [conf 参数](#conf-参数) - [ctx 参数](#ctx-参数) +- [注册公共接口](#注册公共接口) +- [注册控制接口](#注册控制接口) - [编写测试用例](#编写测试用例) - [附上 test-nginx 执行流程](#附上-test-nginx-执行流程) - - [注册公共接口](#注册公共接口) - - [注册控制接口](#注册控制接口) ## 检查外部依赖 @@ -308,52 +308,7 @@ function _M.access(conf, ctx) end ``` -## 编写测试用例 - -针对功能,完善各种维度的测试用例,对插件做个全方位的测试吧!插件的测试用例,都在 __t/plugin__ 目录下,可以前去了解。 -项目测试框架采用的 [****test-nginx****](https://github.com/openresty/test-nginx) 。 -一个测试用例 __.t__ 文件,通常用 \__DATA\__ 分割成 序言部分 和 数据部分。这里我们简单介绍下数据部分, -也就是真正测试用例的部分,仍然以 key-auth 插件为例: - -```perl -=== TEST 1: sanity ---- config - location /t { - content_by_lua_block { - local plugin = require("apisix.plugins.key-auth") - local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER) - if not ok then - ngx.say(err) - end - - ngx.say("done") - } - } ---- request -GET /t ---- response_body -done ---- no_error_log -[error] -``` - -一个测试用例主要有三部分内容: - -- 程序代码: Nginx location 的配置内容 -- 输入: http 的 request 信息 -- 输出检查: status ,header ,body ,error_log 检查 - -这里请求 __/t__ ,经过配置文件 __location__ ,调用 __content_by_lua_block__ 指令完成 lua 的脚本,最终返回。 -用例的断言是 response_body 返回 "done",__no_error_log__ 表示会对 Nginx 的 error.log 检查, -必须没有 ERROR 级别的记录。 - -### 附上 test-nginx 执行流程 - -根据我们在 Makefile 里配置的 PATH,和每一个 __.t__ 文件最前面的一些配置项,框架会组装成一个完整的 nginx.conf 文件, -__t/servroot__ 会被当成 Nginx 的工作目录,启动 Nginx 实例。根据测试用例提供的信息,发起 http 请求并检查 http 的返回项, -包括 http status,http response header, http response body 等。 - -### 注册公共接口 +## 注册公共接口 插件可以注册暴露给公网的接口。以 jwt-auth 插件为例,这个插件为了让客户端能够签名,注册了 `GET /apisix/plugin/jwt/sign` 这个接口: @@ -376,7 +331,7 @@ end 注意注册的接口会暴露到外网。 你可能需要使用 [interceptors](plugin-interceptors.md) 来保护它。 -### 注册控制接口 +## 注册控制接口 如果你只想暴露 API 到 localhost 或内网,你可以通过 [Control API](./control-api.md) 来暴露它。 @@ -405,3 +360,48 @@ end ``` 如果你没有改过默认的 control API 配置,这个插件暴露的 `GET /v1/plugin/example-plugin/hello` API 只有通过 `127.0.0.1` 才能访问它。 + +## 编写测试用例 + +针对功能,完善各种维度的测试用例,对插件做个全方位的测试吧!插件的测试用例,都在 __t/plugin__ 目录下,可以前去了解。 +项目测试框架采用的 [****test-nginx****](https://github.com/openresty/test-nginx) 。 +一个测试用例 __.t__ 文件,通常用 \__DATA\__ 分割成 序言部分 和 数据部分。这里我们简单介绍下数据部分, +也就是真正测试用例的部分,仍然以 key-auth 插件为例: + +```perl +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.key-auth") + local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done +--- no_error_log +[error] +``` + +一个测试用例主要有三部分内容: + +- 程序代码: Nginx location 的配置内容 +- 输入: http 的 request 信息 +- 输出检查: status ,header ,body ,error_log 检查 + +这里请求 __/t__ ,经过配置文件 __location__ ,调用 __content_by_lua_block__ 指令完成 lua 的脚本,最终返回。 +用例的断言是 response_body 返回 "done",__no_error_log__ 表示会对 Nginx 的 error.log 检查, +必须没有 ERROR 级别的记录。 + +### 附上 test-nginx 执行流程 + +根据我们在 Makefile 里配置的 PATH,和每一个 __.t__ 文件最前面的一些配置项,框架会组装成一个完整的 nginx.conf 文件, +__t/servroot__ 会被当成 Nginx 的工作目录,启动 Nginx 实例。根据测试用例提供的信息,发起 http 请求并检查 http 的返回项, +包括 http status,http response header, http response body 等。 From f7c791dcd18286b6bf7fa8db945f7a59097506fd Mon Sep 17 00:00:00 2001 From: Weichang Yang Date: Tue, 28 Dec 2021 09:00:30 +0800 Subject: [PATCH 225/260] feat(nacos): support grpc java (#5894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tzssangglass Co-authored-by: 罗泽轩 Co-authored-by: yangweichang --- apisix/discovery/nacos/init.lua | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua index d163b3952cd2..3fe8d2be2b42 100644 --- a/apisix/discovery/nacos/init.lua +++ b/apisix/discovery/nacos/init.lua @@ -204,7 +204,8 @@ local function iter_and_add_service(services, values) core.table.insert(services, { service_name = up.service_name, namespace_id = namespace_id, - group_name = group_name + group_name = group_name, + scheme = up.scheme, }) end ::CONTINUE:: @@ -228,6 +229,13 @@ local function get_nacos_services() return services end +local function is_grpc(scheme) + if scheme == 'grpc' or scheme == 'grpcs' then + return true + end + + return false +end local function fetch_full_registry(premature) if premature then @@ -255,6 +263,7 @@ local function fetch_full_registry(premature) local data, err local namespace_id = service_info.namespace_id local group_name = service_info.group_name + local scheme = service_info.scheme or '' local namespace_param = get_namespace_param(service_info.namespace_id) local group_name_param = get_group_name_param(service_info.group_name) local query_path = instance_list_path .. service_info.service_name @@ -281,11 +290,19 @@ local function fetch_full_registry(premature) up_apps[namespace_id] [group_name][service_info.service_name] = nodes end - core.table.insert(nodes, { + + local node = { host = host.ip, port = host.port, weight = host.weight or default_weight, - }) + } + + -- docs: https://github.com/yidongnan/grpc-spring-boot-starter/pull/496 + if is_grpc(scheme) and host.metadata and host.metadata.gRPC_port then + node.port = host.metadata.gRPC_port + end + + core.table.insert(nodes, node) end ::CONTINUE:: From 91064015213577fbe74dfac804125e391bbfd91d Mon Sep 17 00:00:00 2001 From: arabot777 <30978207+arabot777@users.noreply.github.com> Date: Tue, 28 Dec 2021 09:39:20 +0800 Subject: [PATCH 226/260] feat(plugin): add degradation switch for ext-plugin (#5897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tzssangglass Co-authored-by: 罗泽轩 Co-authored-by: liushan03 --- apisix/plugins/ext-plugin/init.lua | 9 ++ docs/en/latest/plugins/ext-plugin-pre-req.md | 1 + docs/zh/latest/plugins/ext-plugin-pre-req.md | 1 + t/plugin/ext-plugin/sanity.t | 122 +++++++++++++++++++ 4 files changed, 133 insertions(+) diff --git a/apisix/plugins/ext-plugin/init.lua b/apisix/plugins/ext-plugin/init.lua index 2a2aee6f7a38..4ba3ce58e6e2 100644 --- a/apisix/plugins/ext-plugin/init.lua +++ b/apisix/plugins/ext-plugin/init.lua @@ -99,6 +99,7 @@ local schema = { }, minItems = 1, }, + allow_degradation = {type = "boolean", default = false} }, } @@ -700,6 +701,10 @@ function _M.communicate(conf, ctx, plugin_name) if not core.string.find(err, "conf token not found") then core.log.error(err) + if conf.allow_degradation then + core.log.warn("Plugin Runner is wrong, allow degradation") + return + end return 503 end @@ -708,6 +713,10 @@ function _M.communicate(conf, ctx, plugin_name) end core.log.error(err) + if conf.allow_degradation then + core.log.warn("Plugin Runner is wrong after " .. tries .. " times retry, allow degradation") + return + end return 503 end diff --git a/docs/en/latest/plugins/ext-plugin-pre-req.md b/docs/en/latest/plugins/ext-plugin-pre-req.md index abee4b47f366..ac9354b936be 100644 --- a/docs/en/latest/plugins/ext-plugin-pre-req.md +++ b/docs/en/latest/plugins/ext-plugin-pre-req.md @@ -43,6 +43,7 @@ The result of external plugins execution will affect the behavior of the current | Name | Type | Requirement | Default | Valid | Description | | --------- | ------------- | ----------- | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | conf | array | optional | | [{"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"}] | The plugins list which will be executed at the plugin runner with their configuration | +| allow_degradation | boolean | optional | false | | Whether to enable plugin degradation when the plugin runner is temporarily unavailable. Allow requests to continue when the value is set to true, default false. | ## How To Enable diff --git a/docs/zh/latest/plugins/ext-plugin-pre-req.md b/docs/zh/latest/plugins/ext-plugin-pre-req.md index b1e18e6b751b..c0e3ee912674 100644 --- a/docs/zh/latest/plugins/ext-plugin-pre-req.md +++ b/docs/zh/latest/plugins/ext-plugin-pre-req.md @@ -42,6 +42,7 @@ External Plugins 执行的结果会影响当前请求的行为。 | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | --------- | ------------- | ----------- | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | conf | array | 可选 | | [{"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"}] | 在 Plugin Runner 内执行的插件列表的配置 | +| allow_degradation | boolean | 可选 | false | | 当 Plugin Runner 临时不可用时是否允许请求继续。当值设置为 true 时则自动允许请求继续,默认值是 false。| ## 如何启用 diff --git a/t/plugin/ext-plugin/sanity.t b/t/plugin/ext-plugin/sanity.t index 2a5e965ffc3a..690ef1902d67 100644 --- a/t/plugin/ext-plugin/sanity.t +++ b/t/plugin/ext-plugin/sanity.t @@ -571,3 +571,125 @@ qr/get conf token: 233 conf: \[(\{"value":"bar","name":"foo"\}|\{"name":"foo","v end } } + + + +=== TEST 19: default allow_degradation +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + + local code, message, res = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "ext-plugin-post-req": { + "conf": [ + {"name":"foo", "value":"bar"}, + {"name":"cat", "value":"dog"} + ] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(message) + return + end + + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 20: ext-plugin wrong, req reject +--- request +GET /hello +--- extra_stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock1; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + ext.go({}) + } + } +--- error_code: 503 +--- error_log eval +qr/failed to connect to the unix socket/ + + + +=== TEST 21: open allow_degradation +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin") + + local code, message, res = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "ext-plugin-post-req": { + "conf": [ + {"name":"foo", "value":"bar"}, + {"name":"cat", "value":"dog"} + ], + "allow_degradation": true + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(message) + return + end + + ngx.say(message) + } + } +--- response_body +passed + + + +=== TEST 22: ext-plugin wrong, req access +--- request +GET /hello +--- extra_stream_config + server { + listen unix:$TEST_NGINX_HTML_DIR/nginx.sock1; + + content_by_lua_block { + local ext = require("lib.ext-plugin") + ext.go({}) + } + } +--- response_body +hello world +--- error_log eval +qr/Plugin Runner.*allow degradation/ From eb8362cef73eea34d4c6f41cb1d90fc7cabc6b2d Mon Sep 17 00:00:00 2001 From: guoqqqi <72343596+guoqqqi@users.noreply.github.com> Date: Tue, 28 Dec 2021 14:07:18 +0800 Subject: [PATCH 227/260] docs: improved `plugin-develop` doc (#5933) --- docs/en/latest/plugin-develop.md | 8 +++++++- docs/zh/latest/plugin-develop.md | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md index cc629e6c7526..550188290f55 100644 --- a/docs/en/latest/plugin-develop.md +++ b/docs/en/latest/plugin-develop.md @@ -439,7 +439,13 @@ function _M.control_api() end ``` -If you don't change the default control API configuration, the plugin will be expose `GET /v1/plugin/example-plugin/hello` which can only be accessed via `127.0.0.1`. +If you don't change the default control API configuration, the plugin will be expose `GET /v1/plugin/example-plugin/hello` which can only be accessed via `127.0.0.1`. Test with the following command: + +```shell +curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello" +``` + +[Read more about control API introduction](./control-api.md) ## write test case diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md index 62ddc1199c18..f7c8df5201b5 100644 --- a/docs/zh/latest/plugin-develop.md +++ b/docs/zh/latest/plugin-develop.md @@ -359,7 +359,13 @@ function _M.control_api() end ``` -如果你没有改过默认的 control API 配置,这个插件暴露的 `GET /v1/plugin/example-plugin/hello` API 只有通过 `127.0.0.1` 才能访问它。 +如果你没有改过默认的 control API 配置,这个插件暴露的 `GET /v1/plugin/example-plugin/hello` API 只有通过 `127.0.0.1` 才能访问它。通过以下命令进行测试: + +```shell +curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello" +``` + +[查看更多有关 control API 介绍](./control-api.md) ## 编写测试用例 From e9cea4407e0308733eae6feb337eba60f249abff Mon Sep 17 00:00:00 2001 From: The-White-Lion <37370573+The-White-Lion@users.noreply.github.com> Date: Wed, 29 Dec 2021 10:15:45 +0800 Subject: [PATCH 228/260] feat: support to use path parameter with plugin's control api (#5934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 罗泽轩 --- apisix/control/router.lua | 15 ++++++++++++++- docs/en/latest/control-api.md | 3 +++ docs/zh/latest/control-api.md | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apisix/control/router.lua b/apisix/control/router.lua index 43fb7e213989..851dbc872f3b 100644 --- a/apisix/control/router.lua +++ b/apisix/control/router.lua @@ -16,6 +16,7 @@ -- local require = require local router = require("apisix.utils.router") +local radixtree = require("resty.radixtree") local builtin_v1_routes = require("apisix.control.v1") local plugin_mod = require("apisix.plugin") local core = require("apisix.core") @@ -154,7 +155,19 @@ function fetch_control_api_router() handler = empty_func, }) - return router.new(routes) + local with_parameter = false + local conf = core.config.local_conf() + if conf.apisix.enable_control and conf.apisix.control then + if conf.apisix.control.router == "radixtree_uri_with_parameter" then + with_parameter = true + end + end + + if with_parameter then + return radixtree.new(routes) + else + return router.new(routes) + end end end -- do diff --git a/docs/en/latest/control-api.md b/docs/en/latest/control-api.md index 86f5e72fbdfb..a2258d64a06c 100644 --- a/docs/en/latest/control-api.md +++ b/docs/en/latest/control-api.md @@ -38,6 +38,9 @@ apisix: port: 9090 ``` +The control API server does not support parameter matching by default, if you want to enable parameter matching in plugin's control API +you can add `router: 'radixtree_uri_with_parameter'` to the `control` section. + Note that the control API server should not be configured to listen to the public traffic! ## Control API Added via plugin diff --git a/docs/zh/latest/control-api.md b/docs/zh/latest/control-api.md index 5791b11e0e3a..bbebf7658b22 100644 --- a/docs/zh/latest/control-api.md +++ b/docs/zh/latest/control-api.md @@ -37,6 +37,8 @@ apisix: port: 9090 ``` +插件的 control API 在默认情况下不支持参数匹配,如果想启用参数匹配功能可以在 control 部分添加 `router: 'radixtree_uri_with_parameter'` + 注意: control API server 不应该被配置成监听公网地址。 ## 通过插件添加的 control API From 0a8de202dcc2573123c31d977e6d8777c79be6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 29 Dec 2021 11:39:52 +0800 Subject: [PATCH 229/260] feat: support registering custom variable (#5941) --- apisix/core/ctx.lua | 28 ++++++++++++--- docs/en/latest/plugin-develop.md | 35 +++++++++++++++---- docs/zh/latest/plugin-develop.md | 23 ++++++++++++ t/core/ctx2.t | 60 ++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 10 deletions(-) diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua index f07665ac3e1a..6e6d3295fc2a 100644 --- a/apisix/core/ctx.lua +++ b/apisix/core/ctx.lua @@ -215,11 +215,19 @@ do key = sub_str(key, 9) val = get_parsed_graphql()[key] - elseif apisix_var_names[key] then - val = ngx.ctx.api_ctx and ngx.ctx.api_ctx[key] - else - val = get_var(key, t._request) + local getter = apisix_var_names[key] + if getter then + if getter == true then + val = ngx.ctx.api_ctx and ngx.ctx.api_ctx[key] + else + -- the getter is registered by ctx.register_var + val = getter(ngx.ctx.api_ctx) + end + + else + val = get_var(key, t._request) + end end if val ~= nil and not no_cacheable_var_names[key] then @@ -239,6 +247,18 @@ do end, } +function _M.register_var(name, getter) + if type(getter) ~= "function" then + error("the getter of registered var should be a function") + end + + if apisix_var_names[name] then + error(name .. " is registered") + end + + apisix_var_names[name] = getter +end + function _M.set_vars_meta(ctx) local var = tablepool.fetch("ctx_var", 0, 32) if not var._cache then diff --git a/docs/en/latest/plugin-develop.md b/docs/en/latest/plugin-develop.md index 550188290f55..fdfa8c5264bf 100644 --- a/docs/en/latest/plugin-develop.md +++ b/docs/en/latest/plugin-develop.md @@ -32,10 +32,11 @@ title: Plugin Develop - [implement the logic](#implement-the-logic) - [conf parameter](#conf-parameter) - [ctx parameter](#ctx-parameter) -- [Register public API](#register-public-api) -- [Register control API](#register-control-api) +- [register public API](#register-public-api) +- [register control API](#register-control-api) +- [register custom variable](#register-custom-variable) - [write test case](#write-test-case) - - [Attach the test-nginx execution process:](#attach-the-test-nginx-execution-process) + - [attach the test-nginx execution process:](#attach-the-test-nginx-execution-process) This documentation is about developing plugin in Lua. For other languages, see [external plugin](./external-plugin.md). @@ -388,7 +389,7 @@ function _M.access(conf, ctx) end ``` -## Register public API +## register public API A plugin can register API which exposes to the public. Take jwt-auth plugin as an example, this plugin registers `GET /apisix/plugin/jwt/sign` to allow client to sign its key: @@ -411,7 +412,7 @@ end Note that the public API is exposed to the public. You may need to use [interceptors](plugin-interceptors.md) to protect it. -## Register control API +## register control API If you only want to expose the API to the localhost or intranet, you can expose it via [Control API](./control-api.md). @@ -447,6 +448,28 @@ curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello" [Read more about control API introduction](./control-api.md) +## register custom variable + +We can use variables in many places of APISIX. For example, customizing log format in http-logger, using it as the key of `limit-*` plugins. In some situations, the builtin variables are not enough. Therefore, APISIX allows developers to register their variables globally, and use them as normal builtin variables. + +For instance, let's register a variable called `a6_labels_zone` to fetch the value of the `zone` label in a route: + +``` +local core = require "apisix.core" + +core.ctx.register_var("a6_labels_zone", function(ctx) + local route = ctx.matched_route and ctx.matched_route.value + if route and route.labels then + return route.labels.zone + end + return nil +end) +``` + +After that, any get operation to `$a6_labels_zone` will call the registered getter to fetch the value. + +Note that the custom variables can't be used in features that depend on the Nginx directive, like `access_log_format`. + ## write test case For functions, write and improve the test cases of various dimensions, do a comprehensive test for your plugin! The @@ -494,7 +517,7 @@ Additionally, there are some convenience testing endpoints which can be found [h Refer the following [document](how-to-build.md#Step-4-Run-Test-Cases) to setup the testing framework. -### Attach the test-nginx execution process: +### attach the test-nginx execution process: According to the path we configured in the makefile and some configuration items at the front of each __.t__ file, the framework will assemble into a complete nginx.conf file. "__t/servroot__" is the working directory of Nginx and start the diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md index f7c8df5201b5..d2babcea43fd 100644 --- a/docs/zh/latest/plugin-develop.md +++ b/docs/zh/latest/plugin-develop.md @@ -33,6 +33,7 @@ title: 插件开发 - [ctx 参数](#ctx-参数) - [注册公共接口](#注册公共接口) - [注册控制接口](#注册控制接口) +- [注册自定义变量](#注册自定义变量) - [编写测试用例](#编写测试用例) - [附上 test-nginx 执行流程](#附上-test-nginx-执行流程) @@ -367,6 +368,28 @@ curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello" [查看更多有关 control API 介绍](./control-api.md) +## 注册自定义变量 + +我们可以在APISIX的许多地方使用变量。例如,在 http-logger 中自定义日志格式,用它作为 `limit-*` 插件的键。在某些情况下,内置的变量是不够的。因此,APISIX允许开发者在全局范围内注册他们的变量,并将它们作为普通的内置变量使用。 + +例如,让我们注册一个叫做 `a6_labels_zone` 的变量来获取路由中 `zone` 标签的值。 + +``` +local core = require "apisix.core" + +core.ctx.register_var("a6_labels_zone", function(ctx) + local route = ctx.matched_route and ctx.matched_route.value + if route and route.labels then + return route.labels.zone + end + return nil +end) +``` + +此后,任何对 `$a6_labels_zone` 的获取操作都会调用注册的获取器来获取数值。 + +注意,自定义变量不能用于依赖 Nginx 指令的功能,如 `access_log_format`。 + ## 编写测试用例 针对功能,完善各种维度的测试用例,对插件做个全方位的测试吧!插件的测试用例,都在 __t/plugin__ 目录下,可以前去了解。 diff --git a/t/core/ctx2.t b/t/core/ctx2.t index 7de032f9ed7f..5922e7c7bf83 100644 --- a/t/core/ctx2.t +++ b/t/core/ctx2.t @@ -316,3 +316,63 @@ Content-Type: application/x-www-form-urlencoded --- error_code: 404 --- response_body {"error_msg":"404 Route Not Found"} + + + +=== TEST 15: register custom variable +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [=[{ + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "plugins": { + "serverless-pre-function": { + "phase": "rewrite", + "functions" : ["return function(conf, ctx) ngx.say('find ctx.var.a6_labels_zone: ', ctx.var.a6_labels_zone) end"] + } + }, + "uri": "/hello", + "labels": { + "zone": "Singapore" + } + }]=] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } + + + +=== TEST 16: hit +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local core = require "apisix.core" + core.ctx.register_var("a6_labels_zone", function(ctx) + local route = ctx.matched_route and ctx.matched_route.value + if route and route.labels then + return route.labels.zone + end + return nil + end) + local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/hello" + local httpc = http.new() + local res = assert(httpc:request_uri(uri)) + ngx.print(res.body) + } + } +--- response_body +find ctx.var.a6_labels_zone: Singapore From 4a66b798038bf4dd9834798051d0138e49b40708 Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Wed, 29 Dec 2021 20:19:02 +0800 Subject: [PATCH 230/260] feat: support send APISIX data to assist decision in OPA plugin (#5874) --- apisix/plugins/opa.lua | 8 +- apisix/plugins/opa/helper.lua | 67 ++++++++++-- ci/pod/docker-compose.yml | 5 +- ci/pod/opa/echo.rego | 20 ++++ t/plugin/opa2.t | 187 ++++++++++++++++++++++++++++++++++ 5 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 ci/pod/opa/echo.rego create mode 100644 t/plugin/opa2.t diff --git a/apisix/plugins/opa.lua b/apisix/plugins/opa.lua index cfe0b5810d47..b56403ba4ccb 100644 --- a/apisix/plugins/opa.lua +++ b/apisix/plugins/opa.lua @@ -38,7 +38,10 @@ local schema = { }, keepalive = {type = "boolean", default = true}, keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, - keepalive_pool = {type = "integer", minimum = 1, default = 5} + keepalive_pool = {type = "integer", minimum = 1, default = 5}, + with_route = {type = "boolean", default = false}, + with_service = {type = "boolean", default = false}, + with_consumer = {type = "boolean", default = false}, }, required = {"host", "policy"} } @@ -59,9 +62,10 @@ end function _M.access(conf, ctx) local body = helper.build_opa_input(conf, ctx, "http") + local params = { method = "POST", - body = body, + body = core.json.encode(body), headers = { ["Content-Type"] = "application/json", }, diff --git a/apisix/plugins/opa/helper.lua b/apisix/plugins/opa/helper.lua index 059ea0826203..dc3d12d8d3dc 100644 --- a/apisix/plugins/opa/helper.lua +++ b/apisix/plugins/opa/helper.lua @@ -15,12 +15,15 @@ -- limitations under the License. -- -local core = require("apisix.core") -local ngx_time = ngx.time +local core = require("apisix.core") +local get_service = require("apisix.http.service").get +local ngx_time = ngx.time local _M = {} +-- build a table of Nginx variables with some generality +-- between http subsystem and stream subsystem local function build_var(conf, ctx) return { server_addr = ctx.var.server_addr, @@ -45,16 +48,68 @@ local function build_http_request(conf, ctx) end -function _M.build_opa_input(conf, ctx, subsystem) - local request = build_http_request(conf, ctx) +local function build_http_route(conf, ctx, remove_upstream) + local route = core.table.clone(ctx.matched_route).value + + if remove_upstream and route and route.upstream then + route.upstream = nil + end + + return route +end + + +local function build_http_service(conf, ctx) + local service_id = ctx.service_id + + -- possible that there is no service bound to the route + if service_id then + local service = core.table.clone(get_service(service_id)).value + + if service then + if service.upstream then + service.upstream = nil + end + return service + end + end + + return nil +end + +local function build_http_consumer(conf, ctx) + -- possible that there is no consumer bound to the route + if ctx.consumer then + return core.table.clone(ctx.consumer) + end + + return nil +end + + +function _M.build_opa_input(conf, ctx, subsystem) local data = { type = subsystem, - request = request, + request = build_http_request(conf, ctx), var = build_var(conf, ctx) } - return core.json.encode({input = data}) + if conf.with_route then + data.route = build_http_route(conf, ctx, true) + end + + if conf.with_consumer then + data.consumer = build_http_consumer(conf, ctx) + end + + if conf.with_service then + data.service = build_http_service(conf, ctx) + end + + return { + input = data, + } end diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index 16497f237fe6..e7eabcf80a70 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -402,11 +402,14 @@ services: restart: unless-stopped ports: - 8181:8181 - command: run -s /example.rego /data.json + command: run -s /example.rego /echo.rego /data.json volumes: - type: bind source: ./ci/pod/opa/example.rego target: /example.rego + - type: bind + source: ./ci/pod/opa/echo.rego + target: /echo.rego - type: bind source: ./ci/pod/opa/data.json target: /data.json diff --git a/ci/pod/opa/echo.rego b/ci/pod/opa/echo.rego new file mode 100644 index 000000000000..611f64febbd3 --- /dev/null +++ b/ci/pod/opa/echo.rego @@ -0,0 +1,20 @@ +# +# 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. +# +package echo + +allow = false +reason = input diff --git a/t/plugin/opa2.t b/t/plugin/opa2.t new file mode 100644 index 000000000000..c9ad7f22a97b --- /dev/null +++ b/t/plugin/opa2.t @@ -0,0 +1,187 @@ +# +# 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(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: setup all-in-one test +--- config + location /t { + content_by_lua_block { + local datas = { + { + url = "/apisix/admin/upstreams/u1", + data = [[{ + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }]], + }, + { + url = "/apisix/admin/consumers", + data = [[{ + "username": "test", + "plugins": { + "key-auth": { + "disable": false, + "key": "test-key" + } + } + }]], + }, + { + url = "/apisix/admin/services/s1", + data = [[{ + "name": "s1", + "plugins": { + "key-auth": { + "disable": false + } + } + }]], + }, + { + url = "/apisix/admin/routes/1", + data = [[{ + "plugins": { + "opa": { + "host": "http://127.0.0.1:8181", + "policy": "echo", + "with_route": true, + "with_consumer": true, + "with_service": true + } + }, + "upstream_id": "u1", + "service_id": "s1", + "uri": "/hello" + }]], + }, + } + + local t = require("lib.test_admin").test + + for _, data in ipairs(datas) do + local code, body = t(data.url, ngx.HTTP_PUT, data.data) + ngx.say(code..body) + end + } + } +--- response_body eval +"201passed\n" x 4 + + + +=== TEST 2: hit route (test route data) +--- request +GET /hello +--- more_headers +test-header: only-for-test +apikey: test-key +--- error_code: 403 +--- response_body eval +qr/\"route\":/ and qr/\"id\":\"r1\"/ and qr/\"plugins\":\{\"opa\"/ and +qr/\"with_route\":true/ + + + +=== TEST 3: hit route (test consumer data) +--- request +GET /hello +--- more_headers +test-header: only-for-test +apikey: test-key +--- error_code: 403 +--- response_body eval +qr/\"consumer\":/ and qr/\"username\":\"test\"/ and qr/\"key\":\"test-key\"/ + + + +=== TEST 4: hit route (test service data) +--- request +GET /hello +--- more_headers +test-header: only-for-test +apikey: test-key +--- error_code: 403 +--- response_body eval +qr/\"service\":/ and qr/\"id\":\"s1\"/ and qr/\"query\":\"apikey\"/ and +qr/\"header\":\"apikey\"/ + + + +=== TEST 5: setup route without service +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "opa": { + "host": "http://127.0.0.1:8181", + "policy": "echo", + "with_route": true, + "with_consumer": true, + "with_service": true + } + }, + "upstream_id": "u1", + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 6: hit route (test without service and consumer) +--- request +GET /hello +--- more_headers +test-header: only-for-test +apikey: test-key +--- error_code: 403 +--- response_body_unlike eval +qr/\"service\"/ and qr/\"consumer\"/ From 719c5388143d73cc1424d0a03ec51584a60e243e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 21:06:02 +0800 Subject: [PATCH 231/260] chore(deps): bump actions/setup-node from 2.5.0 to 2.5.1 (#5952) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/doc-lint.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 685077b8880f..6f97fb29d1bf 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2.4.0 - name: 🚀 Use Node.js - uses: actions/setup-node@v2.5.0 + uses: actions/setup-node@v2.5.1 with: node-version: '12.x' - run: npm install -g markdownlint-cli@0.25.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9ab6bf41f00f..37d6385b3324 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Setup Nodejs env - uses: actions/setup-node@v2.5.0 + uses: actions/setup-node@v2.5.1 with: node-version: '12' From 24276b046854b381e0df05843098286d215fe1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Thu, 30 Dec 2021 16:56:59 +0800 Subject: [PATCH 232/260] docs: update document reference link (#5965) --- docs/en/latest/FAQ.md | 2 +- docs/en/latest/grpc-proxy.md | 2 +- docs/en/latest/plugins/grpc-transcode.md | 2 +- docs/en/latest/router-radixtree.md | 4 ++-- docs/zh/latest/README.md | 4 ++-- docs/zh/latest/plugins/grpc-transcode.md | 2 +- docs/zh/latest/router-radixtree.md | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md index 615b2e7b86c0..c9d29d59f629 100644 --- a/docs/en/latest/FAQ.md +++ b/docs/en/latest/FAQ.md @@ -119,7 +119,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335 ``` Here is the operator list of current `lua-resty-radixtree`: -https://github.com/iresty/lua-resty-radixtree#operator-list +https://github.com/api7/lua-resty-radixtree#operator-list 2. Use `traffic-split` plugin to do it. diff --git a/docs/en/latest/grpc-proxy.md b/docs/en/latest/grpc-proxy.md index 3ec23bb844e2..5446263919d3 100644 --- a/docs/en/latest/grpc-proxy.md +++ b/docs/en/latest/grpc-proxy.md @@ -38,7 +38,7 @@ Here's an example, to proxying gRPC service by specified route: * attention: the `scheme` of the route's upstream must be `grpc` or `grpcs`. * attention: APISIX use TLS‑encrypted HTTP/2 to expose gRPC service, so need to [config SSL certificate](certificate.md) * attention: APISIX also support to expose gRPC service with plaintext HTTP/2, which does not rely on TLS, usually used to proxy gRPC service in intranet environment -* the grpc server example:[grpc_server_example](https://github.com/iresty/grpc_server_example) +* the grpc server example:[grpc_server_example](https://github.com/api7/grpc_server_example) ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/docs/en/latest/plugins/grpc-transcode.md b/docs/en/latest/plugins/grpc-transcode.md index cf3d5069d877..2f576ef8d7f7 100644 --- a/docs/en/latest/plugins/grpc-transcode.md +++ b/docs/en/latest/plugins/grpc-transcode.md @@ -67,7 +67,7 @@ curl http://127.0.0.1:9080/apisix/admin/proto/1 -H 'X-API-KEY: edd1c9f034335f136 Here's an example, to enable the grpc-transcode plugin to specified route: * attention: the `scheme` in the route's upstream must be `grpc` -* the grpc server example:[grpc_server_example](https://github.com/iresty/grpc_server_example) +* the grpc server example:[grpc_server_example](https://github.com/api7/grpc_server_example) ```shell curl http://127.0.0.1:9080/apisix/admin/routes/111 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/docs/en/latest/router-radixtree.md b/docs/en/latest/router-radixtree.md index b9f5dea5d7f5..adaa0fe69a93 100644 --- a/docs/en/latest/router-radixtree.md +++ b/docs/en/latest/router-radixtree.md @@ -23,7 +23,7 @@ title: Router radixtree ### what's libradixtree? -[libradixtree](https://github.com/iresty/lua-resty-radixtree), adaptive radix trees implemented in Lua for OpenResty. +[libradixtree](https://github.com/api7/lua-resty-radixtree), adaptive radix trees implemented in Lua for OpenResty. APISIX using libradixtree as route dispatching library. @@ -193,7 +193,7 @@ For more details, see https://github.com/api7/lua-resty-radixtree/#parameters-in ### How to filter route by Nginx builtin variable -Please take a look at [radixtree-new](https://github.com/iresty/lua-resty-radixtree#new), +Please take a look at [radixtree-new](https://github.com/api7/lua-resty-radixtree#new), here is an simple example: ```shell diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index 88d46fd2d929..497d99d5d091 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -98,8 +98,8 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - [支持全路径匹配和前缀匹配](../../en/latest/router-radixtree.md#how-to-use-libradixtree-in-apisix) - [支持使用 Nginx 所有内置变量做为路由的条件](../../en/latest/router-radixtree.md#how-to-filter-route-by-nginx-builtin-variable),所以你可以使用 `cookie`, `args` 等做为路由的条件,来实现灰度发布、A/B 测试等功能 - - 支持[各类操作符做为路由的判断条件](https://github.com/iresty/lua-resty-radixtree#operator-list),比如 `{"arg_age", ">", 24}` - - 支持[自定义路由匹配函数](https://github.com/iresty/lua-resty-radixtree/blob/master/t/filter-fun.t#L10) + - 支持[各类操作符做为路由的判断条件](https://github.com/api7/lua-resty-radixtree#operator-list),比如 `{"arg_age", ">", 24}` + - 支持[自定义路由匹配函数](https://github.com/api7/lua-resty-radixtree/blob/master/t/filter-fun.t#L10) - IPv6:支持使用 IPv6 格式匹配路由 - 支持路由的[自动过期(TTL)](admin-api.md#route) - [支持路由的优先级](../../en/latest/router-radixtree.md#3-match-priority) diff --git a/docs/zh/latest/plugins/grpc-transcode.md b/docs/zh/latest/plugins/grpc-transcode.md index e2a5f541dea4..d031943784d5 100644 --- a/docs/zh/latest/plugins/grpc-transcode.md +++ b/docs/zh/latest/plugins/grpc-transcode.md @@ -67,7 +67,7 @@ curl http://127.0.0.1:9080/apisix/admin/proto/1 -H 'X-API-KEY: edd1c9f034335f136 在指定 route 中,代理 grpc 服务接口: * 注意: 这个 route 对应的 upstream 的属性 `scheme` 必须设置为 `grpc` -* 代理 grpc 服务例子可参考:[grpc_server_example](https://github.com/iresty/grpc_server_example) +* 代理 grpc 服务例子可参考:[grpc_server_example](https://github.com/api7/grpc_server_example) ```shell curl http://127.0.0.1:9080/apisix/admin/routes/111 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/docs/zh/latest/router-radixtree.md b/docs/zh/latest/router-radixtree.md index 2bee1cf5dcfa..ff2dbdeccff6 100644 --- a/docs/zh/latest/router-radixtree.md +++ b/docs/zh/latest/router-radixtree.md @@ -23,7 +23,7 @@ title: 路由 RadixTree ### 什么是 libradixtree? -[libradixtree](https://github.com/iresty/lua-resty-radixtree), 是在 `Lua` 中为 `OpenResty` 实现的自适应 +[libradixtree](https://github.com/api7/lua-resty-radixtree), 是在 `Lua` 中为 `OpenResty` 实现的自适应 [基数树](https://zh.wikipedia.org/wiki/%E5%9F%BA%E6%95%B0%E6%A0%91) 。 `Apache APISIX` 使用 `libradixtree` 作为路由调度库。 @@ -194,7 +194,7 @@ apisix: ### 如何通过 Nginx 内置变量过滤路由 -具体参数及使用方式请查看 [radixtree#new](https://github.com/iresty/lua-resty-radixtree#new) 文档 +具体参数及使用方式请查看 [radixtree#new](https://github.com/api7/lua-resty-radixtree#new) 文档 ,下面是一个简单的示例: ```shell From 7045f9715569b19d334cd7b4bc79ba10e9084fae Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Thu, 30 Dec 2021 17:09:13 +0800 Subject: [PATCH 233/260] chore(jwt-auth): get JWT by ctx.var.cookie_jwt instead of resty.cookie (#5947) --- apisix/plugins/jwt-auth.lua | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/apisix/plugins/jwt-auth.lua b/apisix/plugins/jwt-auth.lua index bf52fa094fae..86a907ed558b 100644 --- a/apisix/plugins/jwt-auth.lua +++ b/apisix/plugins/jwt-auth.lua @@ -16,7 +16,6 @@ -- local core = require("apisix.core") local jwt = require("resty.jwt") -local ck = require("resty.cookie") local consumer_mod = require("apisix.consumer") local resty_random = require("resty.random") local vault = require("apisix.core.vault") @@ -188,13 +187,11 @@ local function fetch_jwt_token(ctx) return token end - local cookie, err = ck:new() - if not cookie then - return nil, err + local val = ctx.var.cookie_jwt + if not val then + return nil, "JWT not found in cookie" end - - local val, err = cookie:get("jwt") - return val, err + return val end @@ -344,10 +341,7 @@ end function _M.rewrite(conf, ctx) local jwt_token, err = fetch_jwt_token(ctx) if not jwt_token then - if err and err:sub(1, #"no cookie") ~= "no cookie" then - core.log.error("failed to fetch JWT token: ", err) - end - + core.log.info("failed to fetch JWT token: ", err) return 401, {message = "Missing JWT token in request"} end From 6ac80b9fb224c150975786a77c0667974d3349e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 30 Dec 2021 18:04:09 +0800 Subject: [PATCH 234/260] docs: avoid newline in the middle of Chinese sentences (#5948) --- .github/workflows/doc-lint.yml | 11 ++++ docs/zh/latest/admin-api.md | 3 +- docs/zh/latest/architecture-design/plugin.md | 18 ++---- docs/zh/latest/architecture-design/route.md | 3 +- .../zh/latest/architecture-design/upstream.md | 3 +- docs/zh/latest/plugin-develop.md | 7 +-- docs/zh/latest/router-radixtree.md | 3 +- docs/zh/latest/stand-alone.md | 6 +- utils/fix-zh-doc-segment.py | 59 +++++++++++++++++++ 9 files changed, 84 insertions(+), 29 deletions(-) create mode 100755 utils/fix-zh-doc-segment.py diff --git a/.github/workflows/doc-lint.yml b/.github/workflows/doc-lint.yml index 6f97fb29d1bf..2244e3721600 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -22,3 +22,14 @@ jobs: - name: check category run: | ./utils/check-category.py + - name: check Chinese doc + run: | + sudo pip3 install zhon + ./utils/fix-zh-doc-segment.py > \ + /tmp/check.log 2>&1 || (cat /tmp/check.log && exit 1) + if grep "find broken newline in file: " /tmp/check.log; then + cat /tmp/error.log + echo "Newline can't appear in the middle of Chinese sentences." + echo "You need to run ./utils/fix-zh-doc-segment.py to fix them." + exit 1 + fi diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 826a908f3302..404604e7e712 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -47,8 +47,7 @@ Admin API 是为 Apache APISIX 服务的一组 API,我们可以将参数传递 *地址*:/apisix/admin/routes/{id}?ttl=0 -*说明*:Route 字面意思就是路由,通过定义一些规则来匹配客户端的请求,然后根据匹配结果加载并执行相应的 -插件,并把请求转发给到指定 Upstream。 +*说明*:Route 字面意思就是路由,通过定义一些规则来匹配客户端的请求,然后根据匹配结果加载并执行相应的插件,并把请求转发给到指定 Upstream。 注意:在启用 `Admin API` 时,它会占用前缀为 `/apisix/admin` 的 API。因此,为了避免您设计 API 与 `/apisix/admin` 冲突,建议为 Admin API 使用其他端口,您可以在 `conf/config.yaml` 中通过 `port_admin` 进行自定义 Admin API 端口。 diff --git a/docs/zh/latest/architecture-design/plugin.md b/docs/zh/latest/architecture-design/plugin.md index 24f5885d2a7f..433e7bd4c184 100644 --- a/docs/zh/latest/architecture-design/plugin.md +++ b/docs/zh/latest/architecture-design/plugin.md @@ -23,15 +23,12 @@ title: Plugin `Plugin` 表示将在 `HTTP` 请求/响应生命周期期间执行的插件配置。 -`Plugin` 配置可直接绑定在 `Route` 上,也可以被绑定在 `Service` 或 `Consumer`上。而对于同一 -个插件的配置,只能有一份是有效的,配置选择优先级总是 `Consumer` > `Route` > `Service`。 +`Plugin` 配置可直接绑定在 `Route` 上,也可以被绑定在 `Service` 或 `Consumer`上。而对于同一个插件的配置,只能有一份是有效的,配置选择优先级总是 `Consumer` > `Route` > `Service`。 -在 `conf/config.yaml` 中,可以声明本地 APISIX 节点都支持哪些插件。这是个白名单机制,不在该 -白名单的插件配置,都将会被自动忽略。这个特性可用于临时关闭或打开特定插件,应对突发情况非常有效。 +在 `conf/config.yaml` 中,可以声明本地 APISIX 节点都支持哪些插件。这是个白名单机制,不在该白名单的插件配置,都将会被自动忽略。这个特性可用于临时关闭或打开特定插件,应对突发情况非常有效。 如果你想在现有插件的基础上新增插件,注意需要拷贝 `conf/config-default.yaml` 的插件节点内容到 `conf/config.yaml` 的插件节点中。 -插件的配置可以被直接绑定在指定 Route 中,也可以被绑定在 Service 中,不过 Route 中的插件配置 -优先级更高。 +插件的配置可以被直接绑定在指定 Route 中,也可以被绑定在 Service 中,不过 Route 中的插件配置优先级更高。 一个插件在一次请求中只会执行一次,即使被同时绑定到多个不同对象中(比如 Route 或 Service)。 插件运行先后顺序是根据插件自身的优先级来决定的,例如: @@ -46,8 +43,7 @@ local _M = { } ``` -插件配置作为 Route 或 Service 的一部分提交的,放到 `plugins` 下。它内部是使用插件 -名字作为哈希的 key 来保存不同插件的配置项。 +插件配置作为 Route 或 Service 的一部分提交的,放到 `plugins` 下。它内部是使用插件名字作为哈希的 key 来保存不同插件的配置项。 ```json { @@ -64,8 +60,7 @@ local _M = { } ``` -并不是所有插件都有具体配置项,比如 `prometheus` 下是没有任何具体配置项,这时候用一个空的对象 -标识即可。 +并不是所有插件都有具体配置项,比如 `prometheus` 下是没有任何具体配置项,这时候用一个空的对象标识即可。 如果一个请求因为某个插件而被拒绝,会有类似这样的 warn 日志:`ip-restriction exits with http status code 403`。 @@ -79,8 +74,7 @@ APISIX 的插件是热加载的,不管你是新增、删除还是修改插件 curl http://127.0.0.1:9080/apisix/admin/plugins/reload -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT ``` -注意:如果你已经在路由规则里配置了某个插件(比如在 `route` 的 `plugins` 字段里面添加了它),然后 -禁用了该插件,在执行路由规则的时候会跳过这个插件。 +注意:如果你已经在路由规则里配置了某个插件(比如在 `route` 的 `plugins` 字段里面添加了它),然后禁用了该插件,在执行路由规则的时候会跳过这个插件。 ## stand-alone 模式下的热加载 diff --git a/docs/zh/latest/architecture-design/route.md b/docs/zh/latest/architecture-design/route.md index 2fb3a8898bc5..86264f87954d 100644 --- a/docs/zh/latest/architecture-design/route.md +++ b/docs/zh/latest/architecture-design/route.md @@ -21,8 +21,7 @@ title: Route # --> -Route 字面意思就是路由,通过定义一些规则来匹配客户端的请求,然后根据匹配结果加载并执行相应的 -插件,并把请求转发给到指定 Upstream。 +Route 字面意思就是路由,通过定义一些规则来匹配客户端的请求,然后根据匹配结果加载并执行相应的插件,并把请求转发给到指定 Upstream。 Route 中主要包含三部分内容:匹配规则(比如 uri、host、remote_addr 等),插件配(限流限速等)和上游信息。 请看下图示例,是一些 Route 规则的实例,当某些属性值相同时,图中用相同颜色标识。 diff --git a/docs/zh/latest/architecture-design/upstream.md b/docs/zh/latest/architecture-design/upstream.md index ca1733edb231..f8561cec8dcf 100644 --- a/docs/zh/latest/architecture-design/upstream.md +++ b/docs/zh/latest/architecture-design/upstream.md @@ -27,8 +27,7 @@ Upstream 是虚拟主机抽象,对给定的多个服务节点按照配置规 如上图所示,通过创建 Upstream 对象,在 `Route` 用 ID 方式引用,就可以确保只维护一个对象的值了。 -Upstream 的配置可以被直接绑定在指定 `Route` 中,也可以被绑定在 `Service` 中,不过 `Route` 中的配置 -优先级更高。这里的优先级行为与 `Plugin` 非常相似 +Upstream 的配置可以被直接绑定在指定 `Route` 中,也可以被绑定在 `Service` 中,不过 `Route` 中的配置优先级更高。这里的优先级行为与 `Plugin` 非常相似 ### 配置参数 diff --git a/docs/zh/latest/plugin-develop.md b/docs/zh/latest/plugin-develop.md index d2babcea43fd..1ef7c5c017ea 100644 --- a/docs/zh/latest/plugin-develop.md +++ b/docs/zh/latest/plugin-develop.md @@ -53,9 +53,7 @@ nginx_config: 插件本身提供了 init 方法。方便插件加载后做初始化动作。 -注:如果部分插件的功能实现,需要在 Nginx 初始化启动,则可能需要在 __apisix/init.lua__ 文件的初始化方法 http_init 中添加逻辑,并且 -可能需要在 __apisix/cli/ngx_tpl.lua__ 文件中,对 Nginx 配置文件生成的部分,添加一些你需要的处理。但是这样容易对全局产生影响,根据现有的 -插件机制,**我们不建议这样做,除非你已经对代码完全掌握**。 +注:如果部分插件的功能实现,需要在 Nginx 初始化启动,则可能需要在 __apisix/init.lua__ 文件的初始化方法 http_init 中添加逻辑,并且可能需要在 __apisix/cli/ngx_tpl.lua__ 文件中,对 Nginx 配置文件生成的部分,添加一些你需要的处理。但是这样容易对全局产生影响,根据现有的插件机制,**我们不建议这样做,除非你已经对代码完全掌握**。 ## 插件命名,优先级和其他 @@ -123,8 +121,7 @@ local _M = { ## 配置描述与校验 -定义插件的配置项,以及对应的 [JSON Schema](https://json-schema.org) 描述,并完成对 JSON 的校验,这样方便对配置的数据规 -格进行验证,以确保数据的完整性以及程序的健壮性。同样,我们以 example-plugin 插件为例,看看他的配置数据: +定义插件的配置项,以及对应的 [JSON Schema](https://json-schema.org) 描述,并完成对 JSON 的校验,这样方便对配置的数据规格进行验证,以确保数据的完整性以及程序的健壮性。同样,我们以 example-plugin 插件为例,看看他的配置数据: ```json { diff --git a/docs/zh/latest/router-radixtree.md b/docs/zh/latest/router-radixtree.md index ff2dbdeccff6..546258f502af 100644 --- a/docs/zh/latest/router-radixtree.md +++ b/docs/zh/latest/router-radixtree.md @@ -194,8 +194,7 @@ apisix: ### 如何通过 Nginx 内置变量过滤路由 -具体参数及使用方式请查看 [radixtree#new](https://github.com/api7/lua-resty-radixtree#new) 文档 -,下面是一个简单的示例: +具体参数及使用方式请查看 [radixtree#new](https://github.com/api7/lua-resty-radixtree#new) 文档,下面是一个简单的示例: ```shell $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' diff --git a/docs/zh/latest/stand-alone.md b/docs/zh/latest/stand-alone.md index 5821fa9145ff..07f7425afad7 100644 --- a/docs/zh/latest/stand-alone.md +++ b/docs/zh/latest/stand-alone.md @@ -28,16 +28,14 @@ title: Stand-alone mode 1. kubernetes(k8s):声明式 API 场景,通过全量 yaml 配置来动态更新修改路由规则。 2. 不同配置中心:配置中心的实现有很多,比如 Consul 等,使用全量 yaml 做中间转换桥梁。 -APISIX 节点服务启动后会立刻加载 `conf/apisix.yaml` 文件中的路由规则到内存,并且每间隔一定时间 -(默认 1 秒钟),都会尝试检测文件内容是否有更新,如果有更新则重新加载规则。 +APISIX 节点服务启动后会立刻加载 `conf/apisix.yaml` 文件中的路由规则到内存,并且每间隔一定时间(默认 1 秒钟),都会尝试检测文件内容是否有更新,如果有更新则重新加载规则。 *注意*:重新加载规则并更新时,均是内存热更新,不会有工作进程的替换过程,是个热更新过程。 由于目前 Admin API 都是基于 etcd 配置中心解决方案,当开启 Stand-alone 模式后, Admin API 将不再被允许使用。 -通过设置 `conf/config.yaml` 中的 `apisix.config_center` 选项为 `yaml` ,并禁用 Admin API 即可启 -用 Stand-alone 模式。 +通过设置 `conf/config.yaml` 中的 `apisix.config_center` 选项为 `yaml` ,并禁用 Admin API 即可启用 Stand-alone 模式。 参考下面示例: diff --git a/utils/fix-zh-doc-segment.py b/utils/fix-zh-doc-segment.py new file mode 100755 index 000000000000..da0ccf684724 --- /dev/null +++ b/utils/fix-zh-doc-segment.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# coding: utf-8 +# +# 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. +# +import os +from os import path +from zhon.hanzi import punctuation # sudo pip3 install zhon + + +def need_fold(pre, cur): + pre = pre.rstrip("\r\n") + if len(pre) == 0 or len(cur) == 0: + return False + if ord(pre[-1]) < 128 or ord(cur[0]) < 128: + return False + # the prev line ends with Chinese and the curr line starts with Chinese + if pre.startswith(":::note"): + # ignore special mark + return False + if pre[-1] in punctuation: + # skip punctuation + return False + return True + +def check_segment(root): + for parent, dirs, files in os.walk(root): + for fn in files: + fn = path.join(parent, fn) + with open(fn) as f: + lines = f.readlines() + new_lines = [lines[0]] + for i in range(1, len(lines)): + if need_fold(lines[i-1], lines[i]): + new_lines[-1] = new_lines[-1].rstrip("\r\n") + lines[i] + else: + new_lines.append(lines[i]) + if len(new_lines) != len(lines): + print("find broken newline in file: %s" % fn) + with open(fn, "w") as f: + f.writelines(new_lines) + + +roots = ["docs/zh/latest/"] +for r in roots: + check_segment(r) From 70ef2d0a44b8f9ed3060ab8ed097118b78661279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 30 Dec 2021 18:04:35 +0800 Subject: [PATCH 235/260] change: don't promise to support Tengine (#5961) --- .github/workflows/build.yml | 1 - README.md | 2 - apisix/cli/ops.lua | 2 +- ci/linux_tengine_runner.sh | 254 ------------------------- docs/en/latest/install-dependencies.md | 2 - docs/en/latest/plugins/dubbo-proxy.md | 2 - docs/zh/latest/README.md | 2 - docs/zh/latest/install-dependencies.md | 2 - docs/zh/latest/plugins/dubbo-proxy.md | 1 - 9 files changed, 1 insertion(+), 267 deletions(-) delete mode 100755 ci/linux_tengine_runner.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69fe2737d9be..55160cc607ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,6 @@ jobs: - linux_openresty - linux_openresty_1_17 - linux_openresty_mtls - - linux_tengine runs-on: ${{ matrix.platform }} timeout-minutes: 90 diff --git a/README.md b/README.md index 23d2cb2dadaa..f87804ff8b7b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - **All platforms** - Cloud-Native: Platform agnostic, No vendor lock-in, APISIX can run from bare-metal to Kubernetes. - - Run Environment: Both OpenResty and Tengine are supported. - Supports ARM64: Don't worry about the lock-in of the infra technology. - **Multi protocols** @@ -221,7 +220,6 @@ Using AWS's eight-core server, APISIX's QPS reaches 140,000 with a latency of on | Plug-in hot loading | Yes | No | | Custom LB and route | Yes | No | | REST API <--> gRPC transcoding | Yes | No | -| Tengine | Yes | No | | MQTT | Yes | No | | Configuration effective time | Event-driven, < 1ms | polling, 5 seconds | | Dashboard | Yes | No | diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 4db2adf4e378..764ed361714d 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -227,7 +227,7 @@ Please modify "admin_key" in conf/config.yaml . util.die("can not find openresty\n") end - local need_ver = "1.17.3" + local need_ver = "1.17.8" if not version_greater_equal(or_ver, need_ver) then util.die("openresty version must >=", need_ver, " current ", or_ver, "\n") end diff --git a/ci/linux_tengine_runner.sh b/ci/linux_tengine_runner.sh deleted file mode 100755 index 116c4f026c7f..000000000000 --- a/ci/linux_tengine_runner.sh +++ /dev/null @@ -1,254 +0,0 @@ -#!/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. -# - -. ./ci/common.sh - -before_install() { - sudo cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1) - - # launch deps env - make ci-env-up - ./ci/linux-ci-init-service.sh -} - -tengine_install() { - if [ -d "build-cache${OPENRESTY_PREFIX}" ]; then - # sudo rm -rf build-cache${OPENRESTY_PREFIX} - sudo mkdir -p ${OPENRESTY_PREFIX} - sudo cp -r build-cache${OPENRESTY_PREFIX}/* ${OPENRESTY_PREFIX}/ - ls -l ${OPENRESTY_PREFIX}/ - ls -l ${OPENRESTY_PREFIX}/bin - return - fi - - export OPENRESTY_VERSION=1.17.8.2 - wget https://openresty.org/download/openresty-$OPENRESTY_VERSION.tar.gz - tar zxf openresty-$OPENRESTY_VERSION.tar.gz - wget https://codeload.github.com/alibaba/tengine/tar.gz/2.3.2 - tar zxf 2.3.2 - - rm -rf openresty-$OPENRESTY_VERSION/bundle/nginx-1.17.8 - mv tengine-2.3.2 openresty-$OPENRESTY_VERSION/bundle/ - - sed -i 's/= auto_complete "nginx";/= auto_complete "tengine";/g' openresty-$OPENRESTY_VERSION/configure - - cd openresty-$OPENRESTY_VERSION - - # patching start - # https://github.com/alibaba/tengine/issues/1381#issuecomment-541493008 - # other patches for tengine 2.3.2 from upstream openresty - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-always_enable_cc_feature_tests.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-balancer_status_code.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-cache_manager_exit.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-daemon_destroy_pool.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-delayed_posted_events.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-hash_overflow.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-init_cycle_pool_release.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-larger_max_error_str.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-log_escape_non_ascii.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-no_Werror.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-pcre_conf_opt.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-proxy_host_port_vars.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-resolver_conf_parsing.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-reuseport_close_unused_fds.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-safe_resolver_ipv6_option.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-single_process_graceful_exit.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-ssl_cert_cb_yield.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-ssl_sess_cb_yield.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-stream_balancer_export.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-stream_proxy_get_next_upstream_tries.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-stream_proxy_timeout_fields.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-stream_ssl_preread_no_skip.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-upstream_pipelining.patch - wget -P patches https://raw.githubusercontent.com/openresty/openresty/master/patches/nginx-1.17.4-upstream_timeout_fields.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/openresty/master/patches/tengine-2.3.2-privileged_agent_process.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-delete_unused_variable.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-keepalive_post_request_status.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-tolerate_backslash_zero_in_uri.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-avoid-limit_req_zone-directive-in-multiple-variables.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-segmentation-fault-in-master-process.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-support-dtls-offload.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-support-prometheus-to-upstream_check_module.patch - wget -P patches https://raw.githubusercontent.com/totemofwolf/tengine/feature/patches/tengine-2.3.2-vnswrr-adaptated-to-dynamic_resolve.patch - - cd bundle/tengine-2.3.2 - patch -p1 < ../../patches/nginx-1.17.4-always_enable_cc_feature_tests.patch - patch -p1 < ../../patches/nginx-1.17.4-balancer_status_code.patch - patch -p1 < ../../patches/nginx-1.17.4-cache_manager_exit.patch - patch -p1 < ../../patches/nginx-1.17.4-daemon_destroy_pool.patch - patch -p1 < ../../patches/nginx-1.17.4-delayed_posted_events.patch - patch -p1 < ../../patches/nginx-1.17.4-hash_overflow.patch - patch -p1 < ../../patches/nginx-1.17.4-init_cycle_pool_release.patch - patch -p1 < ../../patches/nginx-1.17.4-larger_max_error_str.patch - patch -p1 < ../../patches/nginx-1.17.4-log_escape_non_ascii.patch - patch -p1 < ../../patches/nginx-1.17.4-no_Werror.patch - patch -p1 < ../../patches/nginx-1.17.4-pcre_conf_opt.patch - patch -p1 < ../../patches/nginx-1.17.4-proxy_host_port_vars.patch - patch -p1 < ../../patches/nginx-1.17.4-resolver_conf_parsing.patch - patch -p1 < ../../patches/nginx-1.17.4-reuseport_close_unused_fds.patch - patch -p1 < ../../patches/nginx-1.17.4-safe_resolver_ipv6_option.patch - patch -p1 < ../../patches/nginx-1.17.4-single_process_graceful_exit.patch - patch -p1 < ../../patches/nginx-1.17.4-ssl_cert_cb_yield.patch - patch -p1 < ../../patches/nginx-1.17.4-ssl_sess_cb_yield.patch - patch -p1 < ../../patches/nginx-1.17.4-stream_balancer_export.patch - patch -p1 < ../../patches/nginx-1.17.4-stream_proxy_get_next_upstream_tries.patch - patch -p1 < ../../patches/nginx-1.17.4-stream_proxy_timeout_fields.patch - patch -p1 < ../../patches/nginx-1.17.4-stream_ssl_preread_no_skip.patch - patch -p1 < ../../patches/nginx-1.17.4-upstream_pipelining.patch - patch -p1 < ../../patches/nginx-1.17.4-upstream_timeout_fields.patch - patch -p1 < ../../patches/tengine-2.3.2-privileged_agent_process.patch - patch -p1 < ../../patches/tengine-2.3.2-delete_unused_variable.patch - patch -p1 < ../../patches/tengine-2.3.2-keepalive_post_request_status.patch - patch -p1 < ../../patches/tengine-2.3.2-tolerate_backslash_zero_in_uri.patch - patch -p1 < ../../patches/tengine-2.3.2-avoid-limit_req_zone-directive-in-multiple-variables.patch - patch -p1 < ../../patches/tengine-2.3.2-segmentation-fault-in-master-process.patch - patch -p1 < ../../patches/tengine-2.3.2-support-dtls-offload.patch - patch -p1 < ../../patches/tengine-2.3.2-support-prometheus-to-upstream_check_module.patch - patch -p1 < ../../patches/tengine-2.3.2-vnswrr-adaptated-to-dynamic_resolve.patch - - cd - - # patching end - - ./configure --prefix=${OPENRESTY_PREFIX} --with-debug \ - --with-compat \ - --with-file-aio \ - --with-threads \ - --with-http_addition_module \ - --with-http_auth_request_module \ - --with-http_dav_module \ - --with-http_degradation_module \ - --with-http_flv_module \ - --with-http_gunzip_module \ - --with-http_gzip_static_module \ - --with-http_mp4_module \ - --with-http_random_index_module \ - --with-http_realip_module \ - --with-http_secure_link_module \ - --with-http_ssl_module \ - --with-http_stub_status_module \ - --with-http_sub_module \ - --with-http_v2_module \ - --with-stream \ - --with-stream_ssl_module \ - --with-stream_realip_module \ - --with-stream_ssl_preread_module \ - --with-stream_sni \ - --with-pcre \ - --with-pcre-jit \ - --without-mail_pop3_module \ - --without-mail_imap_module \ - --without-mail_smtp_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_upstream_vnswrr_module/ \ - --add-module=bundle/tengine-2.3.2/modules/mod_dubbo \ - --add-module=bundle/tengine-2.3.2/modules/ngx_multi_upstream_module \ - --add-module=bundle/tengine-2.3.2/modules/mod_config \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_concat_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_footer_filter_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_proxy_connect_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_reqstat_module \ - --add-dynamic-module=bundle/tengine-2.3.2/modules/ngx_http_slice_module \ - --add-dynamic-module=bundle/tengine-2.3.2/modules/ngx_http_sysguard_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_trim_filter_module \ - --add-dynamic-module=bundle/tengine-2.3.2/modules/ngx_http_upstream_check_module \ - --add-dynamic-module=bundle/tengine-2.3.2/modules/ngx_http_upstream_consistent_hash_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_upstream_dynamic_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_upstream_dyups_module \ - --add-dynamic-module=bundle/tengine-2.3.2/modules/ngx_http_upstream_session_sticky_module \ - --add-module=bundle/tengine-2.3.2/modules/ngx_http_user_agent_module \ - --add-dynamic-module=bundle/tengine-2.3.2/modules/ngx_slab_stat \ - > build.log 2>&1 || (cat build.log && exit 1) - - make > build.log 2>&1 || (cat build.log && exit 1) - - sudo PATH=$PATH make install > build.log 2>&1 || (cat build.log && exit 1) - - cd .. - - mkdir -p build-cache${OPENRESTY_PREFIX} - cp -r ${OPENRESTY_PREFIX}/* build-cache${OPENRESTY_PREFIX} - ls build-cache${OPENRESTY_PREFIX} - rm -rf openresty-${OPENRESTY_VERSION} - - wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add - - sudo apt-get -y update --fix-missing - sudo apt-get -y install software-properties-common - sudo add-apt-repository -y "deb https://openresty.org/package/ubuntu $(lsb_release -sc) main" - sudo apt-get update - sudo apt-get install openresty-openssl-debug-dev -} - -do_install() { - export_or_prefix - - sudo apt-get -y update --fix-missing - sudo apt-get -y install software-properties-common - - sudo apt-get update - sudo apt-get install lua5.1 liblua5.1-0-dev - - tengine_install - - ./utils/linux-install-luarocks.sh - - create_lua_deps - - ./utils/linux-install-etcd-client.sh - - git clone https://github.com/iresty/test-nginx.git test-nginx -} - -script() { - export_or_prefix - openresty -V - - - ./t/grpc_server_example/grpc_server_example & - - ./bin/apisix help - ./bin/apisix init - ./bin/apisix init_etcd - ./bin/apisix start - mkdir -p logs - sleep 1 - ./bin/apisix stop - sleep 1 - # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t -} - -after_success() { - cat luacov.stats.out - luacov-coveralls -} - -case_opt=$1 -shift - -case ${case_opt} in -before_install) - before_install "$@" - ;; -do_install) - do_install "$@" - ;; -script) - script "$@" - ;; -after_success) - after_success "$@" - ;; -esac diff --git a/docs/en/latest/install-dependencies.md b/docs/en/latest/install-dependencies.md index 048e580b3788..caff62739048 100644 --- a/docs/en/latest/install-dependencies.md +++ b/docs/en/latest/install-dependencies.md @@ -30,8 +30,6 @@ title: Install Dependencies - Now by default Apache APISIX uses HTTP protocol to talk with etcd cluster, which is insecure. Please configure certificate and corresponding private key for your etcd cluster, and use "https" scheme explicitly in the etcd endpoints list in your Apache APISIX configuration, if you want to keep the data secure and integral. See the etcd section in `conf/config-default.yaml` for more details. -- If you want use Tengine instead of OpenResty, please take a look at this installation step script [Install Tengine at Ubuntu](https://github.com/apache/apisix/blob/master/ci/linux_tengine_runner.sh). - - If it is OpenResty 1.19, APISIX will use OpenResty's built-in LuaJIT to run `bin/apisix`; otherwise it will use Lua 5.1. If you encounter `luajit: lj_asm_x86.h:2819: asm_loop_ fixup: Assertion '((intptr_t)target & 15) == 0' failed`, this is a problem with the low version of OpenResty's built-in LuaJIT under certain compilation conditions. - On some platforms, installing LuaRocks via the package manager will cause Lua to be upgraded to Lua 5.3, so we recommend installing LuaRocks via source code. if you install OpenResty and its OpenSSL develop library (openresty-openssl111-devel for rpm and openresty-openssl111-dev for deb) via the official repository, then [we provide a script for automatic installation](https://github.com/apache/apisix/blob/master/utils/linux-install-luarocks.sh). If you compile OpenResty yourself, you can refer to the above script and change the path in it. If you don't specify the OpenSSL library path when you compile, you don't need to configure the OpenSSL variables in LuaRocks, because the system's OpenSSL is used by default. If the OpenSSL library is specified at compile time, then you need to ensure that LuaRocks' OpenSSL configuration is consistent with OpenResty's. diff --git a/docs/en/latest/plugins/dubbo-proxy.md b/docs/en/latest/plugins/dubbo-proxy.md index e13767123cb9..6693b818a9d9 100644 --- a/docs/en/latest/plugins/dubbo-proxy.md +++ b/docs/en/latest/plugins/dubbo-proxy.md @@ -39,8 +39,6 @@ dubbo-proxy plugin allows you proxy HTTP request to [**dubbo**](http://dubbo.apa If you are using OpenResty, you need to build it with dubbo support, see [how to build](../how-to-build.md#step-6-build-openresty-for-apache-apisix) -To make http2dubbo work in APISIX, we enhance the dubbo module based on Tengine's `mod_dubbo`. The modifications are contributed back to Tengine, but they are not included in the latest release version (Tengine-2.3.2) yet. So Tengine itself is unsupported. - ## Runtime Attributes | Name | Type | Requirement | Default | Valid | Description | diff --git a/docs/zh/latest/README.md b/docs/zh/latest/README.md index 497d99d5d091..786669aedbba 100644 --- a/docs/zh/latest/README.md +++ b/docs/zh/latest/README.md @@ -66,7 +66,6 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - **全平台** - 云原生: 平台无关,没有供应商锁定,无论裸机还是 Kubernetes,APISIX 都可以运行。 - - 运行环境: OpenResty 和 Tengine 都支持。 - 支持 ARM64: 不用担心底层技术的锁定。 - **多协议** @@ -219,7 +218,6 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 | 插件热更新 | 是 | 否 | | 用户自定义:负载均衡算法、路由 | 是 | 否 | | resty <--> gRPC 转码 | 是 | 否 | -| 支持 Tengine 作为运行时 | 是 | 否 | | MQTT 协议支持 | 是 | 否 | | 配置生效时间 | 事件通知,低于 1 毫秒更新 | 定期轮询,5 秒 | | 自带控制台 | 是 | 否 | diff --git a/docs/zh/latest/install-dependencies.md b/docs/zh/latest/install-dependencies.md index 1d97dc97092d..203f577610f5 100644 --- a/docs/zh/latest/install-dependencies.md +++ b/docs/zh/latest/install-dependencies.md @@ -30,8 +30,6 @@ title: 安装依赖 - 目前 Apache APISIX 默认使用 HTTP 协议与 etcd 集群通信,这并不安全,如果希望保障数据的安全性和完整性。 请为您的 etcd 集群配置证书及对应私钥,并在您的 Apache APISIX etcd endpoints 配置列表中明确使用 `https` 协议前缀。请查阅 `conf/config-default.yaml` 中 etcd 一节相关的配置来了解更多细节。 -- 如果你要想使用 Tengine 替代 OpenResty,请参考 [Install Tengine at Ubuntu](https://github.com/apache/apisix/blob/master/ci/linux_tengine_runner.sh)。 - - 如果是 OpenResty 1.19,APISIX 会使用 OpenResty 内置的 LuaJIT 来运行 `bin/apisix`;否则会使用 Lua 5.1。如果运行过程中遇到 `luajit: lj_asm_x86.h:2819: asm_loop_fixup: Assertion '((intptr_t)target & 15) == 0' failed`,这是低版本 OpenResty 内置的 LuaJIT 在特定编译条件下的问题。 - 在某些平台上,通过包管理器安装 LuaRocks 会导致 Lua 被升级为 Lua 5.3,所以我们建议通过源代码的方式安装 LuaRocks。如果你通过官方仓库安装 OpenResty 和 OpenResty 的 OpenSSL 开发库(rpm 版本:openresty-openssl111-devel,deb 版本:openresty-openssl111-dev),那么 [我们提供了自动安装的脚本](https://github.com/apache/apisix/tree/master/utils/linux-install-luarocks.sh)。如果你是自己编译的 OpenResty,可以参考上述脚本并修改里面的路径。如果编译时没有指定 OpenSSL 库的路径,那么无需配置 LuaRocks 内跟 OpenSSL 相关的变量,因为默认都是用的系统自带的 OpenSSL。如果编译时指定了 OpenSSL 库,那么需要保证 LuaRocks 的 OpenSSL 配置跟 OpenResty 的相一致。 diff --git a/docs/zh/latest/plugins/dubbo-proxy.md b/docs/zh/latest/plugins/dubbo-proxy.md index 2264672a29b8..5e4a715631c2 100644 --- a/docs/zh/latest/plugins/dubbo-proxy.md +++ b/docs/zh/latest/plugins/dubbo-proxy.md @@ -38,7 +38,6 @@ title: dubbo-proxy ## 要求 如果你正在使用 `OpenResty`, 你需要编译它来支持 `dubbo`, 参考 [如何编译](../how-to-build.md#步骤6:为-Apache-APISIX-构建-OpenResty)。 -在 `APISIX` 中为了实现使从 `http` 代理到 `dubbo`,我们在 `Tengine` 的 `mod_dubbo` 基础上对 `dubbo` 模块做了改进。 所有的修改已经提交给 `Tengine`,但是还未合并到最新的 `release` 版本中(Tengine-2.3.2) 。所以目前 `Tengine` 自身是不支持此特性的。 ## 运行时属性 From 0920d1d21fba36947b4ce13438040874c5d37610 Mon Sep 17 00:00:00 2001 From: Hao Xin Date: Fri, 31 Dec 2021 09:26:44 +0800 Subject: [PATCH 236/260] feat(prometheus): Add metric prefix attr (#5960) --- apisix/plugins/prometheus/exporter.lua | 9 ++++++++- conf/config-default.yaml | 1 + t/cli/test_prometheus.sh | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/apisix/plugins/prometheus/exporter.lua b/apisix/plugins/prometheus/exporter.lua index d565b4c40bd4..a3cc86d28913 100644 --- a/apisix/plugins/prometheus/exporter.lua +++ b/apisix/plugins/prometheus/exporter.lua @@ -16,6 +16,7 @@ -- local base_prometheus = require("prometheus") local core = require("apisix.core") +local plugin = require("apisix.plugin") local ipairs = ipairs local ngx = ngx local ngx_capture = ngx.location.capture @@ -75,7 +76,13 @@ function _M.init() -- We keep the old metric names for the compatibility. -- across all services - prometheus = base_prometheus.init("prometheus-metrics", "apisix_") + local metric_prefix = "apisix_" + local attr = plugin.plugin_attr("prometheus") + if attr and attr.metric_prefix then + metric_prefix = attr.metric_prefix + end + + prometheus = base_prometheus.init("prometheus-metrics", metric_prefix) metrics.connections = prometheus:gauge("nginx_http_current_connections", "Number of HTTP connections", {"state"}) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index ca923baf7e96..cbf97dd66931 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -408,6 +408,7 @@ plugin_attr: endpoint_addr: http://127.0.0.1:12800 prometheus: export_uri: /apisix/prometheus/metrics + metric_prefix: apisix_ enable_export_server: true export_addr: ip: 127.0.0.1 diff --git a/t/cli/test_prometheus.sh b/t/cli/test_prometheus.sh index 7130044f65da..c06cd619f1f6 100755 --- a/t/cli/test_prometheus.sh +++ b/t/cli/test_prometheus.sh @@ -123,3 +123,23 @@ if ! echo "$out" | grep "http listen port 9092 conflicts with prometheus"; then fi echo "passed: should detect port conflicts" + +echo ' +plugin_attr: + prometheus: + metric_prefix: apisix_ci_prefix_ + export_addr: + ip: ${{IP}} + port: ${{PORT}} +' > conf/config.yaml + +IP=127.0.0.1 PORT=9092 make run + +if ! curl -s http://127.0.0.1:9092/apisix/prometheus/metrics | grep "apisix_ci_prefix_" | wc -l; then + echo "failed: should use custom metric prefix" + exit 1 +fi + +make stop + +echo "passed: should use custom metric prefix" From cc09ef3b69089be62718455d9f919879bf28318b Mon Sep 17 00:00:00 2001 From: YuanSheng Wang Date: Fri, 31 Dec 2021 11:13:13 +0800 Subject: [PATCH 237/260] fix: upgrade skywalking to `0.6.0` (#5971) --- rockspec/apisix-master-0.rockspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index d2186dfc520b..883cd42f7647 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -55,7 +55,7 @@ dependencies = { "lua-resty-ipmatcher = 0.6.1", "lua-resty-kafka = 0.07", "lua-resty-logger-socket = 2.0-0", - "skywalking-nginx-lua = 0.5.0", + "skywalking-nginx-lua = 0.6.0", "base64 = 1.5-2", "binaryheap = 0.4", "dkjson = 2.5-2", From b993a7c0e99714224a2b5add18b0a1772c5ead36 Mon Sep 17 00:00:00 2001 From: "Yu.Bozhong" Date: Sun, 2 Jan 2022 10:25:50 +0800 Subject: [PATCH 238/260] chore: remove unuse utils script (#5986) --- utils/gen-install-folder.sh | 17 ----------------- utils/install_yaml_conf.sh | 25 ------------------------- 2 files changed, 42 deletions(-) delete mode 100755 utils/gen-install-folder.sh delete mode 100755 utils/install_yaml_conf.sh diff --git a/utils/gen-install-folder.sh b/utils/gen-install-folder.sh deleted file mode 100755 index 60d2bf5dcd5c..000000000000 --- a/utils/gen-install-folder.sh +++ /dev/null @@ -1,17 +0,0 @@ -# -# 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. -# -find lua -type d | sort | awk '{print "$(INSTALL) -d $(INST_LUADIR)/apisix/" $0 "\n" "$(INSTALL) " $0 "/*.lua $(INST_LUADIR)/apisix/" $0 "/\n" }' diff --git a/utils/install_yaml_conf.sh b/utils/install_yaml_conf.sh deleted file mode 100755 index c369487f90e4..000000000000 --- a/utils/install_yaml_conf.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -# -# 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. -# - -target_file=$1 - -if [ ! -f "$target_file" ]; then - cp ./conf/config.yaml $target_file - chmod 644 $target_file -fi From fea01a78b169028a9ecc932c6ca44bd98c271342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 2 Jan 2022 17:49:31 +0800 Subject: [PATCH 239/260] feat: allow setting proxy_request_buffering without disabling proxy-mirror (#5943) --- .github/workflows/chaos.yml | 6 ++++-- apisix/cli/ngx_tpl.lua | 3 +++ apisix/plugins/proxy-mirror.lua | 17 ++++++++++++++--- t/APISIX.pm | 7 +++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 29af3af83415..1fdaed14fcc1 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -64,9 +64,11 @@ jobs: kubectl create configmap apisix-gw-config.yaml --from-file=./conf/config.yaml kubectl apply -f ./kubernetes/deployment.yaml kubectl apply -f ./kubernetes/service.yaml - kubectl wait pods -l app=apisix-gw --for=condition=Ready --timeout=300s + kubectl wait pods -l app=apisix-gw --for=condition=Ready --timeout=300s \ + || (kubectl logs -l app=apisix-gw && exit 1) kubectl apply -f https://raw.githubusercontent.com/istio/istio/master/samples/httpbin/httpbin.yaml - kubectl wait pods -l app=httpbin --for=condition=Ready --timeout=300s + kubectl wait pods -l app=httpbin --for=condition=Ready --timeout=300s \ + || (kubectl logs -l app=httpbin && exit 1) bash ./t/chaos/utils/setup_chaos_utils.sh port_forward - name: Deploy Chaos Mesh diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 20ba36dd6a2c..b3fd0d6e6285 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -264,6 +264,7 @@ http { {% if use_apisix_openresty then %} apisix_delay_client_max_body_check on; + apisix_mirror_on_demand on; {% end %} access_log {* http.access_log *} main buffer=16384 flush=3; @@ -715,9 +716,11 @@ http { location = /proxy_mirror { internal; + {% if not use_apisix_openresty then %} if ($upstream_mirror_host = "") { return 200; } + {% end %} proxy_http_version 1.1; proxy_set_header Host $upstream_host; diff --git a/apisix/plugins/proxy-mirror.lua b/apisix/plugins/proxy-mirror.lua index f3d857bf8850..7b379a397cce 100644 --- a/apisix/plugins/proxy-mirror.lua +++ b/apisix/plugins/proxy-mirror.lua @@ -16,8 +16,10 @@ -- local core = require("apisix.core") local math_random = math.random -local plugin_name = "proxy-mirror" +local has_mod, apisix_ngx_client = pcall(require, "resty.apisix.client") + +local plugin_name = "proxy-mirror" local schema = { type = "object", properties = { @@ -55,19 +57,28 @@ function _M.check_schema(conf) end +local function enable_mirror(ctx, host) + ctx.var.upstream_mirror_host = host + + if has_mod then + apisix_ngx_client.enable_mirror() + end +end + + function _M.rewrite(conf, ctx) core.log.info("proxy mirror plugin rewrite phase, conf: ", core.json.delay_encode(conf)) ctx.var.upstream_host = ctx.var.host if not conf.sample_ratio or conf.sample_ratio == 1 then - ctx.var.upstream_mirror_host = conf.host + enable_mirror(ctx, conf.host) else local val = math_random() core.log.info("mirror request sample_ratio conf: ", conf.sample_ratio, ", random value: ", val) if val < conf.sample_ratio then - ctx.var.upstream_mirror_host = conf.host + enable_mirror(ctx, conf.host) end end diff --git a/t/APISIX.pm b/t/APISIX.pm index 43dbf92eacab..7138208ac121 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -212,6 +212,7 @@ my $a6_ngx_directives = ""; if ($version =~ m/\/apisix-nginx-module/) { $a6_ngx_directives = <<_EOC_; apisix_delay_client_max_body_check on; + apisix_mirror_on_demand on; wasm_vm wasmtime; _EOC_ } @@ -762,11 +763,17 @@ _EOC_ location = /proxy_mirror { internal; +_EOC_ + if ($version !~ m/\/apisix-nginx-module/) { + $config .= <<_EOC_; if (\$upstream_mirror_host = "") { return 200; } +_EOC_ + } + $config .= <<_EOC_; proxy_http_version 1.1; proxy_set_header Host \$upstream_host; proxy_pass \$upstream_mirror_host\$request_uri; From e7b09064effd5f4b63e8eb6f16651d3504c47031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 2 Jan 2022 17:49:42 +0800 Subject: [PATCH 240/260] ci: rerun flaky tests (#5980) --- ci/centos7-ci.sh | 3 ++- ci/common.sh | 18 ++++++++++++++++++ ci/linux_openresty_common_runner.sh | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index c620417647bf..3167fcec6242 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -85,7 +85,8 @@ run_case() { make init ./utils/set-dns.sh # run test cases - FLUSH_ETCD=1 prove -I./test-nginx/lib -I./ -r t/ + FLUSH_ETCD=1 prove -Itest-nginx/lib -I./ -r t | tee /tmp/test.result + rerun_flaky_tests /tmp/test.result } case_opt=$1 diff --git a/ci/common.sh b/ci/common.sh index 612236e902d5..51bee69a733a 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -31,6 +31,24 @@ create_lua_deps() { # luarocks install luacov-coveralls --tree=deps --local > build.log 2>&1 || (cat build.log && exit 1) } +rerun_flaky_tests() { + if tail -1 "$1" | grep "Result: PASS"; then + exit 0 + fi + + local tests + local n_test + tests="$(awk '/^t\/.*.t\s+\(.+ Failed: .+\)/{ print $1 }' "$1")" + n_test="$(echo "$tests" | wc -l)" + if [ "$n_test" -eq 0 ] || [ "$n_test" -gt 3 ]; then + # CI failure failed test or too many tests failed + exit 1 + fi + + echo "Rerun $(echo "$tests" | xargs)" + FLUSH_ETCD=1 prove -I./test-nginx/lib -I./ $(echo "$tests" | xargs) +} + install_grpcurl () { # For more versions, visit https://github.com/fullstorydev/grpcurl/releases GRPCURL_VERSION="1.8.5" diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index 98a9be25576a..9cfee16fad89 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -83,7 +83,8 @@ script() { done # APISIX_ENABLE_LUACOV=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t - FLUSH_ETCD=1 PERL5LIB=.:$PERL5LIB prove -Itest-nginx/lib -r t + FLUSH_ETCD=1 prove -Itest-nginx/lib -I./ -r t | tee /tmp/test.result + rerun_flaky_tests /tmp/test.result } after_success() { From aa4ea915b48e61033aa78f588a9b30bb07db0247 Mon Sep 17 00:00:00 2001 From: leslie <59061168+leslie-tsang@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:54:25 +0800 Subject: [PATCH 241/260] feat: add separately install support for `install-dependencies.sh` (#5979) --- utils/install-dependencies.sh | 37 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/utils/install-dependencies.sh b/utils/install-dependencies.sh index 9669a81c0489..36623268e083 100755 --- a/utils/install-dependencies.sh +++ b/utils/install-dependencies.sh @@ -125,15 +125,34 @@ function install_luarocks() { # Entry function main() { OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]') - if [[ "${OS_NAME}" == "linux" ]]; then - multi_distro_installation - install_luarocks - install_etcd - elif [[ "${OS_NAME}" == "darwin" ]]; then - install_dependencies_on_mac_osx - else - echo "Non-surported distribution" + if [[ "$#" == 0 ]]; then + if [[ "${OS_NAME}" == "linux" ]]; then + multi_distro_installation + install_luarocks + install_etcd + elif [[ "${OS_NAME}" == "darwin" ]]; then + install_dependencies_on_mac_osx + else + echo "Non-surported distribution" + fi + return fi + + case_opt=$1 + case "${case_opt}" in + "install_etcd") + install_etcd + ;; + "install_luarocks") + install_luarocks + ;; + "multi_distro_installation") + multi_distro_installation + ;; + *) + echo "Unsupported method: ${case_opt}" + ;; + esac } -main +main "$@" From b57420105f518a5ccfe48c723a58d567e20f294e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 3 Jan 2022 19:24:43 +0800 Subject: [PATCH 242/260] fix(datadog): keep consistent with the other logger (#5972) --- apisix/plugins/datadog.lua | 2 -- docs/en/latest/plugins/datadog.md | 4 ++-- t/plugin/datadog.t | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apisix/plugins/datadog.lua b/apisix/plugins/datadog.lua index 8c7062f46592..630d4f125c39 100644 --- a/apisix/plugins/datadog.lua +++ b/apisix/plugins/datadog.lua @@ -39,8 +39,6 @@ local batch_processor_manager = bp_manager_mod.new(plugin_name) local schema = { type = "object", properties = { - max_retry_count = {type = "integer", minimum = 1, default = 1}, - batch_max_size = {type = "integer", minimum = 1, default = 5000}, prefer_name = {type = "boolean", default = true} } } diff --git a/docs/en/latest/plugins/datadog.md b/docs/en/latest/plugins/datadog.md index f99e9b5b6114..d70e71ee35f3 100644 --- a/docs/en/latest/plugins/datadog.md +++ b/docs/en/latest/plugins/datadog.md @@ -49,10 +49,10 @@ For more info on Batch-Processor in Apache APISIX please refer. | Name | Type | Requirement | Default | Valid | Description | | ----------- | ------ | ----------- | ------- | ----- | ------------------------------------------------------------ | | prefer_name | boolean | optional | true | true/false | If set to `false`, would use route/service id instead of name(default) with metric tags. | -| batch_max_size | integer | optional | 5000 | [1,...] | Max buffer size of each batch | +| batch_max_size | integer | optional | 1000 | [1,...] | Max buffer size of each batch | | inactive_timeout | integer | optional | 5 | [1,...] | Maximum age in seconds when the buffer will be flushed if inactive | | buffer_duration | integer | optional | 60 | [1,...] | Maximum age in seconds of the oldest entry in a batch before the batch must be processed | -| max_retry_count | integer | optional | 1 | [1,...] | Maximum number of retries if one entry fails to reach dogstatsd server | +| max_retry_count | integer | optional | 0 | [0,...] | Maximum number of retries if one entry fails to reach dogstatsd server | ## Metadata diff --git a/t/plugin/datadog.t b/t/plugin/datadog.t index 74dafedd1fa4..4eed735ebfde 100644 --- a/t/plugin/datadog.t +++ b/t/plugin/datadog.t @@ -108,7 +108,7 @@ done "plugins": { "datadog": { "batch_max_size" : 1, - "max_retry_count": 1 + "max_retry_count": 0 } }, "upstream": { @@ -126,7 +126,7 @@ done "plugins": { "datadog": { "batch_max_size": 1, - "max_retry_count": 1 + "max_retry_count": 0 } }, "upstream": { From 0918c1e3fa8aa4e194a09854fe5b266fa3cdbe08 Mon Sep 17 00:00:00 2001 From: kerneltravel Date: Mon, 3 Jan 2022 19:40:26 +0800 Subject: [PATCH 243/260] docs: typo fix of traffic-split.md, both en/zh version. (#5993) --- docs/en/latest/plugins/traffic-split.md | 6 +++--- docs/zh/latest/plugins/traffic-split.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/latest/plugins/traffic-split.md b/docs/en/latest/plugins/traffic-split.md index 1d5456bf582b..4dc43b532875 100644 --- a/docs/en/latest/plugins/traffic-split.md +++ b/docs/en/latest/plugins/traffic-split.md @@ -291,9 +291,9 @@ hello 1980 ### Custom Release -Multiple `vars` rules can be set in `match`. Multiple expressions in `vars` have an `add` relationship, and multiple `vars` rules have an `or` relationship; as long as one of the vars is required If the rule passes, the entire `match` passes. +Multiple `vars` rules can be set in `match`. Multiple expressions in `vars` have an `and` relationship, and multiple `vars` rules have an `or` relationship; as long as one of the vars is required If the rule passes, the entire `match` passes. -**Example 1: Only one `vars` rule is configured, and multiple expressions in `vars` are in the relationship of `add`. In `weighted_upstreams`, the traffic is divided into 3:2 according to the value of `weight`, of which only the part of the `weight` value represents the proportion of upstream on the `route`. When `match` fails to pass, all traffic will only hit the upstream on the route.** +**Example 1: Only one `vars` rule is configured, and multiple expressions in `vars` are in the relationship of `and`. In `weighted_upstreams`, the traffic is divided into 3:2 according to the value of `weight`, of which only the part of the `weight` value represents the proportion of upstream on the `route`. When `match` fails to pass, all traffic will only hit the upstream on the route.** ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -370,7 +370,7 @@ hello 1980 After 5 requests, the service of port `1981` was hit 3 times, and the service of port `1980` was hit 2 times. -**Example 2: Configure multiple `vars` rules. Multiple expressions in `vars` are `add` relationships, and multiple `vars` are `or` relationships. According to the `weight` value in `weighted_upstreams`, the traffic is divided into 3:2, where only the part of the `weight` value represents the proportion of upstream on the route. When `match` fails to pass, all traffic will only hit the upstream on the route.** +**Example 2: Configure multiple `vars` rules. Multiple expressions in `vars` are `and` relationships, and multiple `vars` are `or` relationships. According to the `weight` value in `weighted_upstreams`, the traffic is divided into 3:2, where only the part of the `weight` value represents the proportion of upstream on the route. When `match` fails to pass, all traffic will only hit the upstream on the route.** ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' diff --git a/docs/zh/latest/plugins/traffic-split.md b/docs/zh/latest/plugins/traffic-split.md index c7736a2188eb..8a8ee6593ba2 100644 --- a/docs/zh/latest/plugins/traffic-split.md +++ b/docs/zh/latest/plugins/traffic-split.md @@ -291,9 +291,9 @@ hello 1980 ### 自定义发布 -`match` 中可以设置多个 `vars` 规则,`vars` 中的多个表达式之间是 `add` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则整个 `match` 通过。 +`match` 中可以设置多个 `vars` 规则,`vars` 中的多个表达式之间是 `and` 的关系, 多个 `vars` 规则之间是 `or` 的关系;只要其中一个 vars 规则通过,则整个 `match` 通过。 -**示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系。在 `weighted_upstreams` 中根据 `weight` 值将流量按 3:2 划分,其中只有 `weight` 值的部分表示 `route` 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 upstream 。** +**示例1:只配置了一个 `vars` 规则, `vars` 中的多个表达式是 `and` 的关系。在 `weighted_upstreams` 中根据 `weight` 值将流量按 3:2 划分,其中只有 `weight` 值的部分表示 `route` 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 upstream 。** ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -381,7 +381,7 @@ Content-Type: text/html; charset=utf-8 hello 1980 ``` -**示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `add` 的关系, 多个 `vars` 之间是 `or` 的关系。根据 `weighted_upstreams` 中的 `weight` 值将流量按 3:2 划分,其中只有 `weight` 值的部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 upstream 。** +**示例2:配置多个 `vars` 规则, `vars` 中的多个表达式是 `and` 的关系, 多个 `vars` 之间是 `or` 的关系。根据 `weighted_upstreams` 中的 `weight` 值将流量按 3:2 划分,其中只有 `weight` 值的部分表示 route 上的 upstream 所占的比例。 当 `match` 匹配不通过时,所有的流量只会命中 route 上的 upstream 。** ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' From 2f277072cc663d674c1bf988d86f8b429ee1922c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 3 Jan 2022 19:40:40 +0800 Subject: [PATCH 244/260] test(UDP): use longer timeout to avoid 'echo: write error: Broken pipe' (#5989) --- t/cli/test_access_log.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/cli/test_access_log.sh b/t/cli/test_access_log.sh index 1f4cfd544cda..705c4a69096b 100755 --- a/t/cli/test_access_log.sh +++ b/t/cli/test_access_log.sh @@ -251,7 +251,7 @@ echo "passed: stream access_log_format in nginx.conf is ok" make run sleep 0.1 # sending single udp packet -echo -n "hello" | nc -4u -w0 localhost 9200 +echo -n "hello" | nc -4u -w1 localhost 9200 sleep 4 tail -n 1 logs/access_stream.log > output.log From a2628a602e2b5b77258fa6c20232bae998967a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 4 Jan 2022 10:19:22 +0800 Subject: [PATCH 245/260] docs: list APISIX variable in a separate page (#5997) --- docs/en/latest/apisix-variable.md | 42 +++++++++++++++++++++++ docs/en/latest/config.json | 4 +++ docs/en/latest/plugins/http-logger.md | 12 +------ docs/en/latest/plugins/kafka-logger.md | 12 +------ docs/en/latest/plugins/rocketmq-logger.md | 12 +------ docs/zh/latest/plugins/http-logger.md | 12 +------ docs/zh/latest/plugins/kafka-logger.md | 12 +------ docs/zh/latest/plugins/rocketmq-logger.md | 12 +------ 8 files changed, 52 insertions(+), 66 deletions(-) create mode 100644 docs/en/latest/apisix-variable.md diff --git a/docs/en/latest/apisix-variable.md b/docs/en/latest/apisix-variable.md new file mode 100644 index 000000000000..08b1f813799a --- /dev/null +++ b/docs/en/latest/apisix-variable.md @@ -0,0 +1,42 @@ +--- +title: APISIX variable +--- + + + +Besides [Nginx variable](http://nginx.org/en/docs/varindex.html), APISIX also provides +additional variables. + +List in alphabetical order: + +| Variable Name | Description | Example | +|------------------|-------------------------| --------- | +| balancer_ip | the IP of picked upstream server | 1.1.1.1 | +| balancer_port | the port of picked upstream server | 80 | +| consumer_name | username of `consumer` | | +| graphql_name | the [operation name](https://graphql.org/learn/queries/#operation-name) of GraphQL | HeroComparison | +| graphql_operation | the operation type of GraphQL | mutation | +| graphql_root_fields | the top level fields of GraphQL | ["hero"] | +| route_id | id of `route` | | +| route_name | name of `route` | | +| service_id | id of `service` | | +| service_name | name of `service` | | + +You can also [register your own variable](./plugin-develop.md#register-custom-variable). diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 1094fd7348d2..13f4fde95c7f 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -223,6 +223,10 @@ "type": "doc", "id": "plugin-develop" }, + { + "type": "doc", + "id": "apisix-variable" + }, { "type": "doc", "id": "external-plugin" diff --git a/docs/en/latest/plugins/http-logger.md b/docs/en/latest/plugins/http-logger.md index c4f5056aa02e..f4ff56d53690 100644 --- a/docs/en/latest/plugins/http-logger.md +++ b/docs/en/latest/plugins/http-logger.md @@ -91,20 +91,10 @@ hello, world | Name | Type | Requirement | Default | Valid | Description | | ---------------- | ------- | ----------- | ------------- | ------- | ---------------------------------------------------------------------------------------- | -| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get `APISIX` variables or [Nginx variable](http://nginx.org/en/docs/varindex.html). | +| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get [APISIX variable](../apisix-variable.md) or [Nginx variable](http://nginx.org/en/docs/varindex.html). | Note that **the metadata configuration is applied in global scope**, which means it will take effect on all Route or Service which use http-logger plugin. -**APISIX Variables** - -| Variable Name | Description | Usage Example | -|------------------|-------------------------|----------------| -| route_id | id of `route` | $route_id | -| route_name | name of `route` | $route_name | -| service_id | id of `service` | $service_id | -| service_name | name of `service` | $service_name | -| consumer_name | username of `consumer` | $consumer_name | - ### Example ```shell diff --git a/docs/en/latest/plugins/kafka-logger.md b/docs/en/latest/plugins/kafka-logger.md index 504aa116cdc6..5761e3559282 100644 --- a/docs/en/latest/plugins/kafka-logger.md +++ b/docs/en/latest/plugins/kafka-logger.md @@ -183,20 +183,10 @@ hello, world | Name | Type | Requirement | Default | Valid | Description | | ---------------- | ------- | ----------- | ------------- | ------- | ---------------------------------------------------------------------------------------- | -| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get `APISIX` variables or [Nginx variable](http://nginx.org/en/docs/varindex.html). | +| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get [APISIX variable](../apisix-variable.md) or [Nginx variable](http://nginx.org/en/docs/varindex.html). | Note that **the metadata configuration is applied in global scope**, which means it will take effect on all Route or Service which use kafka-logger plugin. -**APISIX Variables** - -| Variable Name | Description | Usage Example | -|------------------|-------------------------|----------------| -| route_id | id of `route` | $route_id | -| route_name | name of `route` | $route_name | -| service_id | id of `service` | $service_id | -| service_name | name of `service` | $service_name | -| consumer_name | username of `consumer` | $consumer_name | - ### Example ```shell diff --git a/docs/en/latest/plugins/rocketmq-logger.md b/docs/en/latest/plugins/rocketmq-logger.md index f17968c4dd51..9699379bc6ed 100644 --- a/docs/en/latest/plugins/rocketmq-logger.md +++ b/docs/en/latest/plugins/rocketmq-logger.md @@ -179,20 +179,10 @@ hello, world | Name | Type | Requirement | Default | Valid | Description | | ---------------- | ------- | ----------- | ------------- | ------- | ---------------------------------------------------------------------------------------- | -| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get `APISIX` variables or [Nginx variable](http://nginx.org/en/docs/varindex.html). | +| log_format | object | optional | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | Log format declared as key value pair in JSON format. Only string is supported in the `value` part. If the value starts with `$`, it means to get [APISIX variables](../apisix-variable.md) or [Nginx variable](http://nginx.org/en/docs/varindex.html). | Note that **the metadata configuration is applied in global scope**, which means it will take effect on all Route or Service which use rocketmq-logger plugin. -**APISIX Variables** - -| Variable Name | Description | Usage Example | -|------------------|-------------------------|----------------| -| route_id | id of `route` | $route_id | -| route_name | name of `route` | $route_name | -| service_id | id of `service` | $service_id | -| service_name | name of `service` | $service_name | -| consumer_name | username of `consumer` | $consumer_name | - ### Example ```shell diff --git a/docs/zh/latest/plugins/http-logger.md b/docs/zh/latest/plugins/http-logger.md index b253355d4fcf..023bdd3c1d1d 100644 --- a/docs/zh/latest/plugins/http-logger.md +++ b/docs/zh/latest/plugins/http-logger.md @@ -91,17 +91,7 @@ hello, world | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | -| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 __APISIX__ 变量或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | - -**APISIX 变量** - -| 变量名 | 描述 | 使用示例 | -|------------------|-------------------------|----------------| -| route_id | `route` 的 id | $route_id | -| route_name | `route` 的 name | $route_name | -| service_id | `service` 的 id | $service_id | -| service_name | `service` 的 name | $service_name | -| consumer_name | `consumer` 的 username | $consumer_name | +| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | ### 设置日志格式示例 diff --git a/docs/zh/latest/plugins/kafka-logger.md b/docs/zh/latest/plugins/kafka-logger.md index 85955330c646..a41b603e300f 100644 --- a/docs/zh/latest/plugins/kafka-logger.md +++ b/docs/zh/latest/plugins/kafka-logger.md @@ -180,17 +180,7 @@ hello, world | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | -| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 __APISIX__ 变量或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | - -**APISIX 变量** - -| 变量名 | 描述 | 使用示例 | -|------------------|-------------------------|----------------| -| route_id | `route` 的 id | $route_id | -| route_name | `route` 的 name | $route_name | -| service_id | `service` 的 id | $service_id | -| service_name | `service` 的 name | $service_name | -| consumer_name | `consumer` 的 username | $consumer_name | +| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | ### 设置日志格式示例 diff --git a/docs/zh/latest/plugins/rocketmq-logger.md b/docs/zh/latest/plugins/rocketmq-logger.md index f61c0b4acf9a..23c74b02f77d 100644 --- a/docs/zh/latest/plugins/rocketmq-logger.md +++ b/docs/zh/latest/plugins/rocketmq-logger.md @@ -177,17 +177,7 @@ hello, world | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | -| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 __APISIX__ 变量或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | - -**APISIX 变量** - -| 变量名 | 描述 | 使用示例 | -|------------------|-------------------------|----------------| -| route_id | `route` 的 id | $route_id | -| route_name | `route` 的 name | $route_name | -| service_id | `service` 的 id | $service_id | -| service_name | `service` 的 name | $service_name | -| consumer_name | `consumer` 的 username | $consumer_name | +| log_format | object | 可选 | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | | 以 JSON 格式的键值对来声明日志格式。对于值部分,仅支持字符串。如果是以 `$` 开头,则表明是要获取 [APISIX 变量](../../../en/latest/apisix-variable.md) 或 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,**该设置是全局生效的**,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 | ### 设置日志格式示例 From d3bb8f9035b14b35e2d2eccd0959ed00e8fe88e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 4 Jan 2022 12:07:58 +0800 Subject: [PATCH 246/260] chore: only reviewer can mark the review resolved (#5994) --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- CONTRIBUTING.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 862a6ce20cda..546bceab0b13 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,13 +5,14 @@ ### Pre-submission checklist: * [ ] Did you explain what problem does this PR solve? Or what new features have been added? diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a41891431d94..c76b6c638af3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,6 +48,7 @@ Once we've discussed your changes and you've got your code ready, make sure that * References the original issue in the description, e.g. "Resolves #123". * Has a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). * Ensure your pull request's title starts from one of the word in the `types` section of [semantic.yml](https://github.com/apache/apisix/blob/master/.github/semantic.yml). +* Follow the [PR manners](https://raw.githubusercontent.com/apache/apisix/master/.github/PULL_REQUEST_TEMPLATE.md) ## Contribution Guidelines for Documentation From c6f1a83c6ff17fdbadd6c7b95f9d1169ce06676e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 5 Jan 2022 09:05:19 +0800 Subject: [PATCH 247/260] feat(limit-count): add constant key type (#5984) Co-authored-by: Yu.Bozhong Co-authored-by: Bisakh --- apisix/plugins/limit-count.lua | 9 ++- docs/en/latest/plugins/limit-count.md | 38 +++++++++++-- docs/zh/latest/plugins/limit-count.md | 37 +++++++++++-- t/plugin/limit-count2.t | 79 +++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 11 deletions(-) diff --git a/apisix/plugins/limit-count.lua b/apisix/plugins/limit-count.lua index 5b0e8c37c9f4..2a5779235b91 100644 --- a/apisix/plugins/limit-count.lua +++ b/apisix/plugins/limit-count.lua @@ -90,7 +90,7 @@ local schema = { group = {type = "string"}, key = {type = "string", default = "remote_addr"}, key_type = {type = "string", - enum = {"var", "var_combination"}, + enum = {"var", "var_combination", "constant"}, default = "var", }, rejected_code = { @@ -238,6 +238,8 @@ function _M.access(conf, ctx) if n_resolved == 0 then key = nil end + elseif conf.key_type == "constant" then + key = conf_key else key = ctx.var[conf_key] end @@ -248,10 +250,11 @@ function _M.access(conf, ctx) key = ctx.var["remote_addr"] end + -- here we add a separator ':' to mark the boundary of the prefix and the key itself if not conf.group then - key = key .. ctx.conf_type .. ctx.conf_version + key = ctx.conf_type .. ctx.conf_version .. ':' .. key else - key = key .. conf.group + key = conf.group .. ':' .. key end core.log.info("limit key: ", key) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index 0062596b3912..e4aafce5391d 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -39,8 +39,8 @@ Limit request rate by a fixed number of requests in a given time window. | ------------------- | ------- | --------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | count | integer | required | | count > 0 | the specified number of requests threshold. | | time_window | integer | required | | time_window > 0 | the time window in seconds before the request count is reset. | -| key_type | string | optional | "var" | ["var", "var_combination"] | the type of key. | -| key | string | optional | "remote_addr" | | the user specified key to limit the rate. If the `key_type` is "var", the key will be treated as a name of variable. If the `key_type` is "var_combination", the key will be a combination of variables. For example, if we use "$remote_addr $consumer_name" as keys, plugin will be restricted by two keys which are "remote_addr" and "consumer_name". If the value of the key is empty, `remote_addr` will be set as the default key.| +| key_type | string | optional | "var" | ["var", "var_combination", "constant"] | the type of key. | +| key | string | optional | "remote_addr" | | the user specified key to limit the rate. If the `key_type` is "constant", the key will be treated as a constant. If the `key_type` is "var", the key will be treated as a name of variable. If the `key_type` is "var_combination", the key will be a combination of variables. For example, if we use "$remote_addr $consumer_name" as key, plugin will be restricted by two variables which are "remote_addr" and "consumer_name". If the value of the key is empty, `remote_addr` will be set as the default key.| | rejected_code | integer | optional | 503 | [200,...,599] | The HTTP status code returned when the request exceeds the threshold is rejected, default 503. | | rejected_msg | string | optional | | non-empty | The response body returned when the request exceeds the threshold is rejected. | | policy | string | optional | "local" | ["local", "redis", "redis-cluster"] | The rate-limiting policies to use for retrieving and incrementing the limits. Available values are `local`(the counters will be stored locally in-memory on the node), `redis`(counters are stored on a Redis server and will be shared across the nodes, usually use it to do the global speed limit), and `redis-cluster` which works the same as `redis` but with redis cluster. | @@ -110,7 +110,7 @@ You also can complete the above operation through the web interface, first add a It is possible to share the same limit counter across different routes. For example, -``` +```shell curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { @@ -133,7 +133,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f0343 Every route which group name is "services_1#1640140620" will share the same count limitation `1` in one minute per remote_addr. -``` +```shell $ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "service_id": "1", @@ -156,6 +156,36 @@ HTTP/1.1 503 ... Note that every limit-count configuration of the same group must be the same. Therefore, once update the configuration, we also need to update the group name. +It is also possible to share the same limit counter in all requests. For example, + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "limit-count": { + "count": 1, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "key_type": "constant", + "group": "services_1#1640140621" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +Compared with the previous configuration, we set the `key_type` to `constant`. +By setting `key_type` to `constant`, we don't evaluate the value of `key` but treat it as a constant. + +Now every route which group name is "services_1#1640140621" will share the same count limitation `1` in one minute among all the requests, +even these requests are from different remote_addr. + If you need a cluster-level precision traffic limit, then we can do it with the redis server. The rate limit of the traffic will be shared between different APISIX nodes to limit the rate of cluster traffic. Here is the example if we use single `redis` policy: diff --git a/docs/zh/latest/plugins/limit-count.md b/docs/zh/latest/plugins/limit-count.md index 4d7273d3e1e6..124b9592f769 100644 --- a/docs/zh/latest/plugins/limit-count.md +++ b/docs/zh/latest/plugins/limit-count.md @@ -42,8 +42,8 @@ title: limit-count | ------------------- | ------- | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | count | integer | 必须 | | count > 0 | 指定时间窗口内的请求数量阈值 | | time_window | integer | 必须 | | time_window > 0 | 时间窗口的大小(以秒为单位),超过这个时间就会重置 | -| key_type | string | 可选 | "var" | ["var", "var_combination"] | key 的类型 | -| key | string | 可选 | "remote_addr" | | 用来做请求计数的依据。如果 `key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 keys,那么插件会同时受 remote_addr 和 consumer_name 两个 key 的约束。如果 key 的值为空,$remote_addr 会被作为默认 key。 | +| key_type | string | 可选 | "var" | ["var", "var_combination", "constant"] | key 的类型 | +| key | string | 可选 | "remote_addr" | | 用来做请求计数的依据。如果 `key_type` 为 "constant",那么 key 会被当作常量。如果 `key_type` 为 "var",那么 key 会被当作变量名称。如果 `key_type` 为 "var_combination",那么 key 会当作变量组。比如如果设置 "$remote_addr $consumer_name" 作为 key,那么插件会同时受 remote_addr 和 consumer_name 两个变量的约束。如果 key 的值为空,$remote_addr 会被作为默认 key。 | | rejected_code | integer | 可选 | 503 | [200,...,599] | 当请求超过阈值被拒绝时,返回的 HTTP 状态码 | | rejected_msg | string | 可选 | | 非空 | 当请求超过阈值被拒绝时,返回的响应体。 | | policy | string | 可选 | "local" | ["local", "redis", "redis-cluster"] | 用于检索和增加限制的速率限制策略。可选的值有:`local`(计数器被以内存方式保存在节点本地,默认选项) 和 `redis`(计数器保存在 Redis 服务节点上,从而可以跨节点共享结果,通常用它来完成全局限速);以及`redis-cluster`,跟 redis 功能一样,只是使用 redis 集群方式。 | @@ -115,7 +115,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 我们也支持在多个 Route 间共享同一个限流计数器。举个例子, -``` +```shell curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "plugins": { @@ -138,7 +138,7 @@ curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f0343 每个配置了 `group` 为 `services_1#1640140620` 的 Route 都将共享同一个每个 IP 地址每分钟只能访问一次的计数器。 -``` +```shell $ curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "service_id": "1", @@ -161,6 +161,35 @@ HTTP/1.1 503 ... 注意同一个 group 里面的 limit-count 配置必须一样。 所以,一旦修改了配置,我们需要更新对应的 group 的值。 +我们也支持在所有请求间共享同一个限流计数器。举个例子, + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/services/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins": { + "limit-count": { + "count": 1, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + "key_type": "constant", + "group": "services_1#1640140621" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +在上面的例子中,我们将 `key_type` 设置为 `constant`。 +通过设置 `key_type` 为 `constant`,`key` 的值将会直接作为常量来处理。 + +现在每个配置了 `group` 为 `services_1#1640140620` 的 Route 上的所有请求,都将共享同一个每分钟只能访问一次的计数器,即使它们来自不同的 IP 地址。 + 如果你需要一个集群级别的流量控制,我们可以借助 redis server 来完成。不同的 APISIX 节点之间将共享流量限速结果,实现集群流量限速。 如果启用单 redis 策略,请看下面例子: diff --git a/t/plugin/limit-count2.t b/t/plugin/limit-count2.t index 3f6bdf325b58..621edad8a912 100644 --- a/t/plugin/limit-count2.t +++ b/t/plugin/limit-count2.t @@ -685,3 +685,82 @@ passed [error] --- response_body {"error_msg":"failed to check the configuration of plugin limit-count err: group conf mismatched"} + + + +=== TEST 20: group with constant key +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/services/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key_type": "constant", + "group": "afafafhao2" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 21: hit multiple paths +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri1 = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local uri2 = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello_chunked" + local ress = {} + for i = 1, 4 do + local httpc = http.new() + local uri + if i % 2 == 1 then + uri = uri1 + else + uri = uri2 + end + + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, res.status) + end + ngx.say(json.encode(ress)) + } + } +--- grep_error_log eval +qr/limit key: afafafhao2:remote_addr/ +--- grep_error_log_out +limit key: afafafhao2:remote_addr +limit key: afafafhao2:remote_addr +limit key: afafafhao2:remote_addr +limit key: afafafhao2:remote_addr +--- response_body +[200,200,503,503] From 3f66fab63ecffa7bf4d5bdb02ce8a688a4285709 Mon Sep 17 00:00:00 2001 From: seven dickens Date: Wed, 5 Jan 2022 09:32:13 +0800 Subject: [PATCH 248/260] fix: resolve unambiguous fully-qualified domain name(FQDN), i.e. ended in a dot (#5999) --- rockspec/apisix-master-0.rockspec | 2 +- t/node/upstream-domain.t | 48 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index 883cd42f7647..6e391d0f4863 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -32,7 +32,7 @@ description = { dependencies = { "lua-resty-ctxdump = 0.1-0", - "lua-resty-dns-client = 5.2.0", + "lua-resty-dns-client = 5.2.3", "lua-resty-template = 2.0", "lua-resty-etcd = 1.6.0", "api7-lua-resty-http = 0.2.0", diff --git a/t/node/upstream-domain.t b/t/node/upstream-domain.t index 63069fda18c5..2cf71dd87696 100644 --- a/t/node/upstream-domain.t +++ b/t/node/upstream-domain.t @@ -399,3 +399,51 @@ GET /t 1980, 1981, 1981 --- no_error_log [error] + + + +=== TEST 15: set route(with upstream) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "nodes": { + "foo.com.": 0, + "127.0.0.1:1980": 1 + }, + "type": "roundrobin", + "desc": "new upstream" + }, + "service_id": "1" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] + + + +=== TEST 16: hit routes, parse the domain of upstream node +--- request +GET /hello +--- response_body +hello world +--- error_log eval +qr/dns resolver domain: foo.com. to \d+.\d+.\d+.\d+/ +--- no_error_log +[error] From 045fa33266310ca9e679ff08b0822e239bfc685d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 6 Jan 2022 08:55:16 +0800 Subject: [PATCH 249/260] test: add missing shdict (#6022) --- t/APISIX.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/t/APISIX.pm b/t/APISIX.pm index 7138208ac121..827275b679f2 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -349,6 +349,7 @@ _EOC_ lua_shared_dict lrucache-lock-stream 10m; lua_shared_dict plugin-limit-conn-stream 10m; + lua_shared_dict etcd-cluster-health-check-stream 10m; upstream apisix_backend { server 127.0.0.1:1900; From aff5d825aaea12eab312e65e82d060ab9be9b06f Mon Sep 17 00:00:00 2001 From: yin6516008 <492960429@qq.com> Date: Thu, 6 Jan 2022 08:58:10 +0800 Subject: [PATCH 250/260] docs: update powered-by.md (#6027) --- powered-by.md | 1 + 1 file changed, 1 insertion(+) diff --git a/powered-by.md b/powered-by.md index 99fef66f98ce..316ab454e0df 100644 --- a/powered-by.md +++ b/powered-by.md @@ -87,6 +87,7 @@ Users are encouraged to add themselves to this page, [issue](https://github.com/ 1. 中食安泓(广东)健康产业有限公司 1. 上海泽怡信息科技 1. 北京新片场传媒股份有限公司 +1. 武汉精臣智慧标识科技有限公司 From 1bfbf245831979378c9dfb9b4c599e3e4eab9639 Mon Sep 17 00:00:00 2001 From: piglei Date: Thu, 6 Jan 2022 14:13:44 +0800 Subject: [PATCH 251/260] docs: tiny enhancements on documentation("getting-started", "plugin" ) (#6016) --- docs/en/latest/getting-started.md | 15 ++++++++------- docs/zh/latest/architecture-design/plugin.md | 4 +++- docs/zh/latest/architecture-design/service.md | 2 +- docs/zh/latest/architecture-design/upstream.md | 2 +- docs/zh/latest/getting-started.md | 15 ++++++++------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/docs/en/latest/getting-started.md b/docs/en/latest/getting-started.md index 919ed5201be3..cd30bfe966d2 100644 --- a/docs/en/latest/getting-started.md +++ b/docs/en/latest/getting-started.md @@ -138,35 +138,36 @@ Apache APISIX provides users with a powerful [Admin API](./admin-api.md) and [AP We can create a [Route](./architecture-design/route.md) and connect it to an Upstream service(also known as the [Upstream](./architecture-design/upstream.md)). When a `Request` arrives at Apache APISIX, Apache APISIX knows which Upstream the request should be forwarded to. -Because we have configured matching rules for the Route object, Apache APISIX can forward the request to the corresponding Upstream service. The following code is an example of a Route configuration: +Because we have configured matching rules for the Route object, Apache APISIX can forward the request to the corresponding Upstream service. The following code creates a sample configuration of Route: -```json +```bash +curl "http://127.0.0.1:9080/apisix/admin/routes/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d ' { "methods": ["GET"], "host": "example.com", - "uri": "/services/users/*", + "uri": "/anything/*", "upstream": { "type": "roundrobin", "nodes": { "httpbin.org:80": 1 } } -} +}' ``` This routing configuration means that all matching inbound requests will be forwarded to the Upstream service `httpbin.org:80` when they meet **all** the rules listed below: - The HTTP method of the request is `GET`. - The request header contains the `host` field, and its value is `example.com`. -- The request path matches `/services/users/*`, `*` means any subpath, for example `/services/users/getAll?limit=10`. +- The request path matches `/anything/*`, `*` means any subpath, for example `/anything/foo?arg=10`. Once this route is created, we can access the Upstream service using the address exposed by Apache APISIX. ```bash -curl -i -X GET "http://{APISIX_BASE_URL}/services/users/getAll?limit=10" -H "Host: example.com" +curl -i -X GET "http://127.0.0.1:9080/anything/foo?arg=10" -H "Host: example.com" ``` -This will be forwarded to `http://httpbin.org:80/services/users/getAll?limit=10` by Apache APISIX. +This will be forwarded to `http://httpbin.org:80/anything/foo?arg=10` by Apache APISIX. ### Create an Upstream diff --git a/docs/zh/latest/architecture-design/plugin.md b/docs/zh/latest/architecture-design/plugin.md index 433e7bd4c184..daddd168e787 100644 --- a/docs/zh/latest/architecture-design/plugin.md +++ b/docs/zh/latest/architecture-design/plugin.md @@ -43,7 +43,9 @@ local _M = { } ``` -插件配置作为 Route 或 Service 的一部分提交的,放到 `plugins` 下。它内部是使用插件名字作为哈希的 key 来保存不同插件的配置项。 +插件配置可存放于 Route 或 Service 中,键为 `plugins`,值是包含多个插件配置的对象。对象内部用插件名字作为 key 来保存不同插件的配置项。 + +如下所示的配置中,包含 `limit-count` 和 `prometheus` 两种插件的配置: ```json { diff --git a/docs/zh/latest/architecture-design/service.md b/docs/zh/latest/architecture-design/service.md index 154c647955ec..e5c15086d978 100644 --- a/docs/zh/latest/architecture-design/service.md +++ b/docs/zh/latest/architecture-design/service.md @@ -85,4 +85,4 @@ curl http://127.0.0.1:9080/apisix/admin/routes/102 -H 'X-API-KEY: edd1c9f034335f }' ``` -注意:当 Route 和 Service 都开启同一个插件时,Route 参数的优先级是高于 Service 的。 +注意:当 Route 和 Service 都开启同一个插件时,Route 中的插件参数会优先于 Service 被使用。 diff --git a/docs/zh/latest/architecture-design/upstream.md b/docs/zh/latest/architecture-design/upstream.md index f8561cec8dcf..8f06caa1d731 100644 --- a/docs/zh/latest/architecture-design/upstream.md +++ b/docs/zh/latest/architecture-design/upstream.md @@ -27,7 +27,7 @@ Upstream 是虚拟主机抽象,对给定的多个服务节点按照配置规 如上图所示,通过创建 Upstream 对象,在 `Route` 用 ID 方式引用,就可以确保只维护一个对象的值了。 -Upstream 的配置可以被直接绑定在指定 `Route` 中,也可以被绑定在 `Service` 中,不过 `Route` 中的配置优先级更高。这里的优先级行为与 `Plugin` 非常相似 +Upstream 的配置可以被直接绑定在指定 `Route` 中,也可以被绑定在 `Service` 中,不过 `Route` 中的配置优先级更高。这里的优先级行为与 `Plugin` 非常相似。 ### 配置参数 diff --git a/docs/zh/latest/getting-started.md b/docs/zh/latest/getting-started.md index b7c996d0927e..1780d1155afd 100644 --- a/docs/zh/latest/getting-started.md +++ b/docs/zh/latest/getting-started.md @@ -136,35 +136,36 @@ Apache APISIX 提供了强大的 [Admin API](./admin-api.md) 和 [Dashboard](htt 我们可以创建一个 [Route](./architecture-design/route.md) 并与上游服务(通常也被称为 [Upstream](./architecture-design/upstream.md) 或后端服务)绑定,当一个 `请求(Request)` 到达 Apache APISIX 时,Apache APISIX 就会明白这个请求应该转发到哪个上游服务中。 -因为我们为 Route 对象配置了匹配规则,所以 Apache APISIX 可以将请求转发到对应的上游服务。以下代码是一个 Route 配置示例: +因为我们为 Route 对象配置了匹配规则,所以 Apache APISIX 可以将请求转发到对应的上游服务。以下代码会创建一个示例 Route 配置: -```json +```bash +curl "http://127.0.0.1:9080/apisix/admin/routes/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d ' { "methods": ["GET"], "host": "example.com", - "uri": "/services/users/*", + "uri": "/anything/*", "upstream": { "type": "roundrobin", "nodes": { "httpbin.org:80": 1 } } -} +}' ``` 这条路由配置意味着,当它们满足下述的 **所有** 规则时,所有匹配的入站请求都将被转发到 `httpbin.org:80` 这个上游服务: - 请求的 HTTP 方法为 `GET`。 - 请求头包含 `host` 字段,且它的值为 `example.com`。 -- 请求路径匹配 `/services/users/*`,`*` 意味着任意的子路径,例如 `/services/users/getAll?limit=10`。 +- 请求路径匹配 `/anything/*`,`*` 意味着任意的子路径,例如 `/anything/foo?arg=10`。 当这条路由创建后,我们可以使用 Apache APISIX 对外暴露的地址去访问上游服务: ```bash -curl -i -X GET "http://{APISIX_BASE_URL}/services/users/getAll?limit=10" -H "Host: example.com" +curl -i -X GET "http://127.0.0.1:9080/anything/foo?arg=10" -H "Host: example.com" ``` -这将会被 Apache APISIX 转发到 `http://httpbin.org:80/services/users/getAll?limit=10`。 +这将会被 Apache APISIX 转发到 `http://httpbin.org:80/anything/foo?arg=10`。 ### 创建上游服务(Upstream) From 58fec8f5eb599c93c3491e200a4ab66907a3dd24 Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Fri, 7 Jan 2022 02:49:15 +0800 Subject: [PATCH 252/260] docs: add OPA plugin document (#5970) --- docs/en/latest/config.json | 3 +- docs/en/latest/plugins/opa.md | 275 ++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 docs/en/latest/plugins/opa.md diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 13f4fde95c7f..dcbfe682cb0c 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -66,7 +66,8 @@ "plugins/openid-connect", "plugins/hmac-auth", "plugins/authz-casbin", - "plugins/ldap-auth" + "plugins/ldap-auth", + "plugins/opa" ] }, { diff --git a/docs/en/latest/plugins/opa.md b/docs/en/latest/plugins/opa.md new file mode 100644 index 000000000000..47f05b80759c --- /dev/null +++ b/docs/en/latest/plugins/opa.md @@ -0,0 +1,275 @@ +--- +title: opa +--- + + + +## Summary + +- [**Description**](#description) +- [**Attributes**](#attributes) +- [**Data Definition**](#data-definition) +- [**Example**](#example) + +## Description + +The `opa` plugin is used to integrate with [Open Policy Agent](https://www.openpolicyagent.org). By using this plugin, users can decouple functions such as authentication and access to services and reduce the complexity of the application system. + +## Attributes + +| Name | Type | Requirement | Default | Valid | Description | +| -- | -- | -- | -- | -- | -- | +| host | string | required | | | Open Policy Agent service host (eg. https://localhost:8181) | +| ssl_verify | boolean | optional | true | | Whether to verify the certificate | +| policy | string | required | | | OPA policy path (It is a combination of `package` and `decision`. When you need to use advanced features such as custom response, `decision` can be omitted) | +| timeout | integer | optional | 3000ms | [1, 60000]ms | HTTP call timeout. | +| keepalive | boolean | optional | true | | HTTP keepalive | +| keepalive_timeout | integer | optional | 60000ms | [1000, ...]ms | keepalive idle timeout | +| keepalive_pool | integer | optional | 5 | [1, ...]ms | Connection pool limit | +| with_route | boolean | optional | false | | Whether to send information about the current route. | +| with_service | boolean | optional | false | | Whether to send information about the current service. | +| with_consumer | boolean | optional | false | | Whether to send information about the current consumer. (It may contain sensitive information such as apikey, so please turn it on only if you are sure it is safe) | + +## Data Definition + +### APISIX to OPA service + +The `type` indicates that the request type. (e.g. `http` or `stream`) +The `reqesut` is used when the request type is `http`, it contains the basic information of the request. (e.g. url, header) +The `var` contains basic information about this requested connection. (e.g. IP, port, request timestamp) +The `route`, `service`, and `consumer` will be sent only after the `opa` plugin has enabled the relevant features, and their contents are same as those stored by APISIX in etcd. + +```json +{ + "type": "http", + "request": { + "scheme": "http", + "path": "\/get", + "headers": { + "user-agent": "curl\/7.68.0", + "accept": "*\/*", + "host": "127.0.0.1:9080" + }, + "query": {}, + "port": 9080, + "method": "GET", + "host": "127.0.0.1" + }, + "var": { + "timestamp": 1701234567, + "server_addr": "127.0.0.1", + "server_port": "9080", + "remote_port": "port", + "remote_addr": "ip address" + }, + "route": {}, + "service": {}, + "consumer": {} +} +``` + +### OPA service response to APISIX + +In the response, `result` is automatically added by OPA. The `allow` is indispensable and will indicate whether the request is allowed to be forwarded through the APISIX. +The `reason`, `headers`, and `status_code` are optional and are only returned when you need to use a custom response, as you'll see in the next section with the actual use case for it. + +```json +{ + "result": { + "allow": true, + "reason": "test", + "headers": { + "an": "header" + }, + "status_code": 401 + } +} +``` + +## Example + +First, you need to launch the Open Policy Agent environment. + +```shell +$ docker run -d --name opa -p 8181:8181 openpolicyagent/opa:0.35.0 run -s +``` + +### Basic Use Case + +You can create a basic policy for testing. + +```shell +$ curl -X PUT '127.0.0.1:8181/v1/policies/example1' \ + -H 'Content-Type: text/plain' \ + -d 'package example1 + +import input.request + +default allow = false + +allow { + # HTTP method must GET + request.method == "GET" +}' +``` + +After that, you can create a route and turn on the `opa` plugin. + +```shell +$ curl -X PUT 'http://127.0.0.1:9080/apisix/admin/routes/r1' \ + -H 'X-API-KEY: ' \ + -H 'Content-Type: application/json' \ + -d '{ + "uri": "/*", + "plugins": { + "opa": { + "host": "http://127.0.0.1:8181", + "policy": "example1" + } + }, + "upstream": { + "nodes": { + "httpbin.org:80": 1 + }, + "type": "roundrobin" + } +}' +``` + +Try it out. + +```shell +# Successful request +$ curl -i -X GET 127.0.0.1:9080/get +HTTP/1.1 200 OK + +# Failed request +$ curl -i -X POST 127.0.0.1:9080/post +HTTP/1.1 403 FORBIDDEN +``` + +### Complex Use Case (custom response) + +Next, let's think about some more complex scenarios. + +When you need to return a custom error message for an incorrect request, you can implement it this way. + +```shell +$ curl -X PUT '127.0.0.1:8181/v1/policies/example2' \ + -H 'Content-Type: text/plain' \ + -d 'package example2 + +import input.request + +default allow = false + +allow { + request.method == "GET" +} + +# custom response body (Accepts a string or an object, the object will respond as JSON format) +reason = "test" { + not allow +} + +# custom response header (The data of the object can be written in this way) +headers = { + "Location": "http://example.com/auth" +} { + not allow +} + +# custom response status code +status_code = 302 { + not allow +}' +``` + +Update the route and set `opa` plugin's `policy` parameter to `example2`. Then, let's try it. + +```shell +# Successful request +$ curl -i -X GET 127.0.0.1:9080/get +HTTP/1.1 200 OK + +# Failed request +$ curl -i -X POST 127.0.0.1:9080/post +HTTP/1.1 302 FOUND +Location: http://example.com/auth + +test +``` + +### Complex Use Case (send APISIX data) + +Let's think about another scenario, when your decision needs to use some APISIX data, such as `route`, `consumer`, etc., how should we do it? + +Create a simple policy `echo`, which will return the data sent by APISIX to the OPA service as is, so we can simply see them. + +```shell +$ curl -X PUT '127.0.0.1:8181/v1/policies/echo' \ + -H 'Content-Type: text/plain' \ + -d 'package echo + +allow = false +reason = input' +``` + +Next, update the config of the route to enable sending route data. + +```shell +$ curl -X PUT 'http://127.0.0.1:9080/apisix/admin/routes/r1' \ + -H 'X-API-KEY: ' \ + -H 'Content-Type: application/json' \ + -d '{ + "uri": "/*", + "plugins": { + "opa": { + "host": "http://127.0.0.1:8181", + "policy": "echo", + "with_route": true + } + }, + "upstream": { + "nodes": { + "httpbin.org:80": 1 + }, + "type": "roundrobin" + } +}' +``` + +Try it. As you can see, we output this data with the help of the custom response body function described above, along with the data from the route. + +```shell +$ curl -X GET 127.0.0.1:9080/get +{ + "type": "http", + "request": { + xxx + }, + "var": { + xxx + }, + "route": { + xxx + } +} +``` From 18c316f8a00f6c125a92df09da95774d5f17f241 Mon Sep 17 00:00:00 2001 From: hf400159 <97138894+hf400159@users.noreply.github.com> Date: Fri, 7 Jan 2022 13:38:43 +0800 Subject: [PATCH 253/260] doc: Update NOTICE (#6038) --- NOTICE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index 024c35ed53d6..13e98eb2019f 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ Apache APISIX -Copyright 2019-2021 The Apache Software Foundation +Copyright 2019-2022 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). From ef04e067c8044b98b261fd51149d9e8503b84d8a Mon Sep 17 00:00:00 2001 From: Daniel Kocot Date: Fri, 7 Jan 2022 10:34:02 +0100 Subject: [PATCH 254/260] docs: added some explanations for the usage of the mqtt proxy (#5888) --- docs/en/latest/plugins/mqtt-proxy.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/latest/plugins/mqtt-proxy.md b/docs/en/latest/plugins/mqtt-proxy.md index 4ef020bc6704..353069cfe256 100644 --- a/docs/en/latest/plugins/mqtt-proxy.md +++ b/docs/en/latest/plugins/mqtt-proxy.md @@ -71,7 +71,6 @@ Creates a stream route, and enable plugin `mqtt-proxy`. ```shell curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { - "remote_addr": "127.0.0.1", "plugins": { "mqtt-proxy": { "protocol_name": "MQTT", @@ -89,6 +88,8 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 }' ``` +In case Docker is used in combination with MacOS `host.docker.internal` is the right parameter for `host`. + ## Delete Plugin ```shell From e01abd9905b3b267078960dacd7653e4ffca3265 Mon Sep 17 00:00:00 2001 From: guoqqqi <72343596+guoqqqi@users.noreply.github.com> Date: Sun, 9 Jan 2022 16:27:13 +0800 Subject: [PATCH 255/260] docs: update some explanations for the usage of the mqtt proxy (zh) (#6051) --- docs/zh/latest/plugins/mqtt-proxy.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/zh/latest/plugins/mqtt-proxy.md b/docs/zh/latest/plugins/mqtt-proxy.md index a320c92c1aa0..7514b2054213 100644 --- a/docs/zh/latest/plugins/mqtt-proxy.md +++ b/docs/zh/latest/plugins/mqtt-proxy.md @@ -64,12 +64,11 @@ title: mqtt-proxy 然后把 MQTT 请求发送到 9100 端口即可。 -下面是一个示例,在指定的 route 上开启了 mqtt-proxy 插件: +下面是一个示例,在指定的 route 上开启了 `mqtt-proxy` 插件: ```shell curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { - "remote_addr": "127.0.0.1", "plugins": { "mqtt-proxy": { "protocol_name": "MQTT", @@ -87,6 +86,8 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 }' ``` +在 Docker 与 MacOS 结合使用的情况下,`host.docker.internal` 是 `host` 的正确参数。 + #### 禁用插件 当你想去掉插件的时候,很简单,在插件的配置中把对应的 json 配置删除即可,无须重启服务,即刻生效: From e8229f51c6ee6c716d3e167f3b82abfd0cb40e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 9 Jan 2022 19:18:54 +0800 Subject: [PATCH 256/260] test: doc & simpify stream part of the test framework (#6041) --- docs/en/latest/internal/testing-framework.md | 64 +++++++++++++++++++- t/APISIX.pm | 5 ++ t/debug/debug-mode.t | 1 - t/stream-node/sanity.t | 3 - t/stream-node/sni.t | 1 - t/stream-plugin/ip-restriction.t | 2 - t/stream-plugin/mqtt-proxy.t | 9 --- 7 files changed, 68 insertions(+), 17 deletions(-) diff --git a/docs/en/latest/internal/testing-framework.md b/docs/en/latest/internal/testing-framework.md index 7b22fd7b16bc..f43ad4098b28 100644 --- a/docs/en/latest/internal/testing-framework.md +++ b/docs/en/latest/internal/testing-framework.md @@ -22,7 +22,7 @@ title: Introducing APISIX's testing framework --> APISIX uses a testing framework based on our fork of test-nginx: https://github.com/iresty/test-nginx. -For details, you can check the documentation of this project. +For details, you can check the [documentation](https://metacpan.org/pod/Test::Nginx) of this project. If you want to test the CLI behavior of APISIX (`./bin/apisix`), you need to write a shell script in the t/cli directory to test it. You can refer to the existing test scripts for more details. @@ -111,6 +111,32 @@ GET /index.html no valid upstream node ``` +## Preparing the upstream + +To test the code, we need to provide a mock upstream. + +For HTTP request, the upstream code is put in `t/lib/server.lua`. HTTP request with +a given `path` will trigger the method in same name. For example, a call to `/server_port` +will call the `_M.server_port`. + +For TCP request, a dummy upstream is used: + +``` +local sock = ngx.req.socket() +local data = sock:receive("1") +ngx.say("hello world") +``` + +If you want to custom the TCP upstream logic, you can use: + +``` +--- stream_upstream_code +local sock = ngx.req.socket() +local data = sock:receive("1") +ngx.sleep(0.2) +ngx.say("hello world") +``` + ## Send request We can initiate a request with `request` and set the request headers with `more_headers`. @@ -180,6 +206,23 @@ Sending multiple requests concurrently: end ``` +## Send TCP request + +We can use `stream_request` to send a TCP request, for example: + +``` +--- stream_request +hello +``` + +To send a TLS over TCP request, we can combine `stream_tls_request` with `stream_sni`: + +``` +--- stream_tls_request +mmm +--- stream_sni: xx.com +``` + ## Assertions The following assertions are commonly used. @@ -205,6 +248,22 @@ Check response body. [{"count":12, "port": "1982"}] ``` +Check the TCP response. + +When the request is sent via `stream_request`: + +``` +--- stream_response +receive stream response error: connection reset by peer +``` + +When the request is sent via `stream_tls_request`: + +``` +--- response_body +receive stream response error: connection reset by peer +``` + Checking the error log (via grep error log with regular expression). ``` @@ -230,7 +289,10 @@ The test framework listens to multiple ports when it is started. * 1980/1981/1982/5044: HTTP upstream port * 1983: HTTPS upstream port * 1984: APISIX HTTP port. Can be used to verify HTTP related gateway logic, such as concurrent access to an API. +* 1985: APISIX TCP port. Can be used to verify TCP related gateway logic, such as concurrent access to an API. * 1994: APISIX HTTPS port. Can be used to verify HTTPS related gateway logic, such as testing certificate matching logic. +* 1995: TCP upstream port +* 2005: APISIX TLS over TCP port. Can be used to verify TLS over TCP related gateway logic, such as concurrent access to an API. The methods in `t/lib/server.lua` are executed when accessing the upstream port. `_M.go` is the entry point for this file. When the request accesses the upstream `/xxx`, the `_M.xxx` method is executed. For example, a request for `/hello` will execute `_M.hello`. diff --git a/t/APISIX.pm b/t/APISIX.pm index 827275b679f2..23daaa32d16f 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -335,6 +335,11 @@ _EOC_ } my $stream_enable = $block->stream_enable; + if ($block->stream_request) { + # Like stream_tls_request, if stream_request is given, automatically enable stream + $stream_enable = 1; + } + my $stream_conf_enable = $block->stream_conf_enable; my $extra_stream_config = $block->extra_stream_config // ''; my $stream_upstream_code = $block->stream_upstream_code // <<_EOC_; diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 026569558546..fea9461a830a 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -348,7 +348,6 @@ passed === TEST 10: hit route --- debug_config eval: $::debug_config ---- stream_enable --- stream_request eval "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- stream_response diff --git a/t/stream-node/sanity.t b/t/stream-node/sanity.t index 58ded0abd9c9..c1e810a18fc2 100644 --- a/t/stream-node/sanity.t +++ b/t/stream-node/sanity.t @@ -57,7 +57,6 @@ passed === TEST 2: hit route ---- stream_enable --- stream_request eval mmm --- stream_response @@ -223,7 +222,6 @@ passed === TEST 9: hit route ---- stream_enable --- stream_request eval mmm --- stream_response @@ -312,7 +310,6 @@ passed === TEST 12: hit route ---- stream_enable --- stream_request eval mmm --- stream_response diff --git a/t/stream-node/sni.t b/t/stream-node/sni.t index ff80c955fdfd..0d71313640bb 100644 --- a/t/stream-node/sni.t +++ b/t/stream-node/sni.t @@ -156,7 +156,6 @@ proxy request to 127.0.0.2:1995 === TEST 5: hit route, no TLS ---- stream_enable --- stream_request mmm --- stream_response diff --git a/t/stream-plugin/ip-restriction.t b/t/stream-plugin/ip-restriction.t index 9f8ea5a6e6d3..75d5053c9f17 100644 --- a/t/stream-plugin/ip-restriction.t +++ b/t/stream-plugin/ip-restriction.t @@ -83,7 +83,6 @@ passed === TEST 2: hit ---- stream_enable --- stream_request eval mmm --- error_log @@ -124,7 +123,6 @@ passed === TEST 4: hit ---- stream_enable --- stream_request eval mmm --- stream_response diff --git a/t/stream-plugin/mqtt-proxy.t b/t/stream-plugin/mqtt-proxy.t index 3aa5cdfda0f0..df3eb10a539c 100644 --- a/t/stream-plugin/mqtt-proxy.t +++ b/t/stream-plugin/mqtt-proxy.t @@ -65,7 +65,6 @@ passed === TEST 2: invalid header ---- stream_enable --- stream_request eval mmm --- error_log @@ -74,7 +73,6 @@ Received unexpected MQTT packet type+flags === TEST 3: hit route ---- stream_enable --- stream_request eval "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- stream_response @@ -123,7 +121,6 @@ passed === TEST 5: failed to match route ---- stream_enable --- stream_request eval "\x10\x0f" --- stream_response @@ -210,7 +207,6 @@ passed === TEST 8: hit route ---- stream_enable --- stream_request eval "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- stream_response @@ -259,7 +255,6 @@ passed === TEST 10: hit route ---- stream_enable --- stream_request eval "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- error_log @@ -311,7 +306,6 @@ passed === TEST 12: hit route ---- stream_enable --- stream_request eval "\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f" --- stream_response @@ -326,7 +320,6 @@ mqtt client id: foo === TEST 13: hit route with empty client id ---- stream_enable --- stream_request eval "\x10\x0c\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x00" --- stream_response @@ -382,7 +375,6 @@ passed === TEST 15: hit route with empty property ---- stream_enable --- stream_request eval "\x10\x0d\x00\x04\x4d\x51\x54\x54\x05\x02\x00\x3c\x00\x00\x00" --- stream_response @@ -396,7 +388,6 @@ qr/mqtt client id: \w+/ === TEST 16: hit route with property ---- stream_enable --- stream_request eval "\x10\x1b\x00\x04\x4d\x51\x54\x54\x05\x02\x00\x3c\x05\x11\x00\x00\x0e\x10\x00\x09\x63\x6c\x69\x6e\x74\x2d\x31\x31\x31" --- stream_response From 609b504112f1dbf200583b3058306becbd67a336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Sun, 9 Jan 2022 23:53:07 +0800 Subject: [PATCH 257/260] ci: rewrite CI job to a regular test (#6052) --- .github/workflows/build.yml | 1 - ci/linux_openresty_mtls_runner.sh | 132 ------------------------------ t/cli/test_admin_mtls.sh | 54 ++++++++++++ 3 files changed, 54 insertions(+), 133 deletions(-) delete mode 100755 ci/linux_openresty_mtls_runner.sh create mode 100755 t/cli/test_admin_mtls.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55160cc607ee..a99778aac334 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,6 @@ jobs: os_name: - linux_openresty - linux_openresty_1_17 - - linux_openresty_mtls runs-on: ${{ matrix.platform }} timeout-minutes: 90 diff --git a/ci/linux_openresty_mtls_runner.sh b/ci/linux_openresty_mtls_runner.sh deleted file mode 100755 index a979f648308a..000000000000 --- a/ci/linux_openresty_mtls_runner.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/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. -# - -. ./ci/common.sh - -before_install() { - sudo cpanm --notest Test::Nginx >build.log 2>&1 || (cat build.log && exit 1) -} - -do_install() { - export_or_prefix - - ./utils/linux-install-openresty.sh - - ./utils/linux-install-luarocks.sh - - ./utils/linux-install-etcd-client.sh - - create_lua_deps - - # sudo apt-get install tree -y - # tree deps - - git clone https://github.com/iresty/test-nginx.git test-nginx -} - -script() { - export_or_prefix - openresty -V - - - # enable mTLS - echo " -apisix: - port_admin: 9180 - https_admin: true - - admin_api_mtls: - admin_ssl_cert: "../t/certs/mtls_server.crt" - admin_ssl_cert_key: "../t/certs/mtls_server.key" - admin_ssl_ca_cert: "../t/certs/mtls_ca.crt" - -" > conf/config.yaml - - ./bin/apisix help - ./bin/apisix init - ./bin/apisix init_etcd - ./bin/apisix start - - sleep 1 - cat logs/error.log - - - echo "127.0.0.1 admin.apisix.dev" | sudo tee -a /etc/hosts - - # correct certs - code=$(curl -i -o /dev/null -s -w %{http_code} --cacert ./t/certs/mtls_ca.crt --key ./t/certs/mtls_client.key --cert ./t/certs/mtls_client.crt -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) - if [ ! $code -eq 200 ]; then - echo "failed: failed to enabled mTLS for admin" - exit 1 - fi - - # # no certs - # code=$(curl -i -o /dev/null -s -w %{http_code} -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) - # if [ ! $code -eq 000 ]; then - # echo "failed: failed to enabled mTLS for admin" - # exit 1 - # fi - - # # no ca cert - # code=$(curl -i -o /dev/null -s -w %{http_code} --key ./t/certs/mtls_client.key --cert ./t/certs/mtls_client.crt -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) - # if [ ! $code -eq 000 ]; then - # echo "failed: failed to enabled mTLS for admin" - # exit 1 - # fi - - # # error key - # code=$(curl -i -o /dev/null -s -w %{http_code} --cacert ./t/certs/mtls_ca.crt --key ./t/certs/mtls_server.key --cert ./t/certs/mtls_client.crt -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) - # if [ ! $code -eq 000 ]; then - # echo "failed: failed to enabled mTLS for admin" - # exit 1 - # fi - - # skip - code=$(curl -i -o /dev/null -s -w %{http_code} -k -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) - if [ ! $code -eq 400 ]; then - echo "failed: failed to enabled mTLS for admin" - exit 1 - fi - - ./bin/apisix stop - sleep 1 -} - -after_success() { - #cat luacov.stats.out - #luacov-coveralls - echo "done" -} - -case_opt=$1 -shift - -case ${case_opt} in -before_install) - before_install "$@" - ;; -do_install) - do_install "$@" - ;; -script) - script "$@" - ;; -after_success) - after_success "$@" - ;; -esac diff --git a/t/cli/test_admin_mtls.sh b/t/cli/test_admin_mtls.sh new file mode 100755 index 000000000000..aa90e5053438 --- /dev/null +++ b/t/cli/test_admin_mtls.sh @@ -0,0 +1,54 @@ +#!/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. +# + +. ./t/cli/common.sh + +# The 'admin.apisix.dev' is injected by utils/set-dns.sh + +echo ' +apisix: + port_admin: 9180 + https_admin: true + + admin_api_mtls: + admin_ssl_cert: "../t/certs/mtls_server.crt" + admin_ssl_cert_key: "../t/certs/mtls_server.key" + admin_ssl_ca_cert: "../t/certs/mtls_ca.crt" + +' > conf/config.yaml + +make run + +sleep 1 + +# correct certs +code=$(curl -i -o /dev/null -s -w %{http_code} --cacert ./t/certs/mtls_ca.crt --key ./t/certs/mtls_client.key --cert ./t/certs/mtls_client.crt -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) +if [ ! "$code" -eq 200 ]; then + echo "failed: failed to enabled mTLS for admin" + exit 1 +fi + +# skip +code=$(curl -i -o /dev/null -s -w %{http_code} -k -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' https://admin.apisix.dev:9180/apisix/admin/routes) +if [ ! "$code" -eq 400 ]; then + echo "failed: failed to enabled mTLS for admin" + exit 1 +fi + +echo "passed: enabled mTLS for admin" From 7db31a1a7186b966bc0f066539d4de8011871012 Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Sun, 9 Jan 2022 23:57:10 +0800 Subject: [PATCH 258/260] feat: release 2.10.3 (#5985) --- CHANGELOG.md | 30 ++++------ docs/zh/latest/CHANGELOG.md | 30 ++++------ rockspec/apisix-2.10.3-0.rockspec | 95 +++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 rockspec/apisix-2.10.3-0.rockspec diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4f86aab636..6cb14bb5152e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ title: Changelog ## Table of Contents - [2.11.0](#2110) +- [2.10.3](#2103) - [2.10.2](#2102) - [2.10.1](#2101) - [2.10.0](#2100) @@ -79,34 +80,23 @@ title: Changelog - :sunrise: feat(ext-plugin): avoid sending conf request more times [#5183](https://github.com/apache/apisix/pull/5183) - :sunrise: feat: Add ldap-auth plugin [#3894](https://github.com/apache/apisix/pull/3894) -## 2.10.2 +## 2.10.3 -### Bugfix +**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.** -- fix: response.set_header should remove header like request.set_header [#5499](https://github.com/apache/apisix/pull/5499) -- fix(batch-requests): correct the client ip in the pipeline [#5476](https://github.com/apache/apisix/pull/5476) -- fix(upstream): load imbalance when it's referred by multiple routes [#5462](https://github.com/apache/apisix/pull/5462) -- fix(hmac-auth): check if the X-HMAC-ALGORITHM header is missing [#5467](https://github.com/apache/apisix/pull/5467) -- fix: prevent being hacked by untrusted request_uri [#5458](https://github.com/apache/apisix/pull/5458) -- fix(admin): modify boolean parameters with PATCH [#5434](https://github.com/apache/apisix/pull/5432) -- fix(traffic-split): multiple rules with multiple weighted_upstreams under each rule cause upstream_key duplicate [#5414](https://github.com/apache/apisix/pull/5414) -- fix: add handler for invalid basic auth header values [#5419](https://github.com/apache/apisix/pull/5419) +[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103) -### Change +## 2.10.2 + +**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.** -- change: log insensitive consumer info only [#5445](https://github.com/apache/apisix/pull/5445) +[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102) ## 2.10.1 -### Bugfix +**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.10` branch.** -- fix(zipkin): response_span doesn't have correct start time [#5295](https://github.com/apache/apisix/pull/5295) -- fix(ext-plugin): don't use stale key [#5309](https://github.com/apache/apisix/pull/5309) -- fix: route's timeout should not be overwrittern by service [#5219](https://github.com/apache/apisix/pull/5219) -- fix: filter nil plugin conf triggered by etcd dir init [#5204](https://github.com/apache/apisix/pull/5204) -- fix: pass correct host header to health checker target nodes [#5175](https://github.com/apache/apisix/pull/5175) -- fix: upgrade lua-resty-balancer to 0.04 [#5144](https://github.com/apache/apisix/pull/5144) -- fix(prometheus): avoid negative latency caused by inconsistent Nginx metrics [#5150](https://github.com/apache/apisix/pull/5150) +[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101) ## 2.10.0 diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index 1de0c071b2e3..f97ba2da36b5 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -24,6 +24,7 @@ title: CHANGELOG ## Table of Contents - [2.11.0](#2110) +- [2.10.3](#2103) - [2.10.2](#2102) - [2.10.1](#2101) - [2.10.0](#2100) @@ -79,34 +80,23 @@ title: CHANGELOG - :sunrise: ext-plugin 避免发送重复的 conf 请求 [#5183](https://github.com/apache/apisix/pull/5183) - :sunrise: 新增 ldap-auth 插件 [#3894](https://github.com/apache/apisix/pull/3894) -## 2.10.2 +## 2.10.3 -### Bugfix +**这是一个 LTS 维护版本,您可以在 `release/2.10` 分支中看到 CHANGELOG。** -- 更正 response.set_header 行为,与 request.set_header 保持一致 [#5499](https://github.com/apache/apisix/pull/5499) -- 修复 batch-requests 插件中 client ip 的问题 [#5476](https://github.com/apache/apisix/pull/5476) -- 修复 upstream 被多条 routes 绑定时,负载不平衡的问题 [#5462](https://github.com/apache/apisix/pull/5462) -- hmac-auth 插件检查是否缺少 X-HMAC-ALGORITHM header [#5467](https://github.com/apache/apisix/pull/5467) -- 防止不可信的 request_uri [#5458](https://github.com/apache/apisix/pull/5458) -- 修复用 PATCH 方法修改 boolean 参数的问题 [#5434](https://github.com/apache/apisix/pull/5432) -- 修复 traffic-split 插件 upstream_key 重复的问题 [#5414](https://github.com/apache/apisix/pull/5414) -- basic-auth 插件处理无效的 Authorization header [#5419](https://github.com/apache/apisix/pull/5419) +[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2103) -### Change +## 2.10.2 + +**这是一个 LTS 维护版本,您可以在 `release/2.10` 分支中看到 CHANGELOG。** -- 只记录不敏感的 consumer 信息 [#5445](https://github.com/apache/apisix/pull/5445) +[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2102) ## 2.10.1 -### Bugfix +**这是一个 LTS 维护版本,您可以在 `release/2.10` 分支中看到 CHANGELOG。** -- 更正 zipkin 插件 response_span 的开始时间 [#5295](https://github.com/apache/apisix/pull/5295) -- 避免发送过期 key 给 plugin runner [#5309](https://github.com/apache/apisix/pull/5309) -- 更正 route 的 timeout 被 service 覆盖的问题 [#5219](https://github.com/apache/apisix/pull/5219) -- 过滤掉初始化 etcd 数据时产生的空 plugin conf [#5204](https://github.com/apache/apisix/pull/5204) -- 健康检查特定情况下会发送错误的 Host header [#5175](https://github.com/apache/apisix/pull/5175) -- 升级 lua-resty-balancer 到 0.04 [#5144](https://github.com/apache/apisix/pull/5144) -- prometheus 插件修复偶发的 latency 为负数的问题 [#5150](https://github.com/apache/apisix/pull/5150) +[https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101](https://github.com/apache/apisix/blob/release/2.10/CHANGELOG.md#2101) ## 2.10.0 diff --git a/rockspec/apisix-2.10.3-0.rockspec b/rockspec/apisix-2.10.3-0.rockspec new file mode 100644 index 000000000000..1823cae3d525 --- /dev/null +++ b/rockspec/apisix-2.10.3-0.rockspec @@ -0,0 +1,95 @@ +-- +-- 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. +-- + +package = "apisix" +version = "2.10.3-0" +supported_platforms = {"linux", "macosx"} + +source = { + url = "git://github.com/apache/apisix", + branch = "2.10.3", +} + +description = { + summary = "Apache APISIX is a cloud-native microservices API gateway, delivering the ultimate performance, security, open source and scalable platform for all your APIs and microservices.", + homepage = "https://github.com/apache/apisix", + license = "Apache License 2.0", +} + +dependencies = { + "lua-resty-ctxdump = 0.1-0", + "lua-resty-dns-client = 5.2.0", + "lua-resty-template = 2.0", + "lua-resty-etcd = 1.5.4", + "api7-lua-resty-http = 0.2.0", + "lua-resty-balancer = 0.04", + "lua-resty-ngxvar = 0.5.2", + "lua-resty-jit-uuid = 0.0.7", + "lua-resty-healthcheck-api7 = 2.2.0", + "lua-resty-jwt = 0.2.0", + "lua-resty-hmac-ffi = 0.05", + "lua-resty-cookie = 0.1.0", + "lua-resty-session = 2.24", + "opentracing-openresty = 0.1", + "lua-resty-radixtree = 2.8.1", + "lua-protobuf = 0.3.3", + "lua-resty-openidc = 1.7.2-1", + "luafilesystem = 1.7.0-2", + "api7-lua-tinyyaml = 0.3.0", + "nginx-lua-prometheus = 0.20210206", + "jsonschema = 0.9.5", + "lua-resty-ipmatcher = 0.6.1", + "lua-resty-kafka = 0.07", + "lua-resty-logger-socket = 2.0-0", + "skywalking-nginx-lua = 0.4-1", + "base64 = 1.5-2", + "binaryheap = 0.4", + "dkjson = 2.5-2", + "resty-redis-cluster = 1.02-4", + "lua-resty-expr = 1.3.1", + "graphql = 0.0.2", + "argparse = 0.7.1-1", + "luasocket = 3.0rc1-2", + "luasec = 0.9-1", + "lua-resty-consul = 0.3-2", + "penlight = 1.9.2-1", + "ext-plugin-proto = 0.3.0", + "casbin = 1.26.0", + "api7-snowflake = 2.0-1", + "inspect == 3.1.1", +} + +build = { + type = "make", + build_variables = { + CFLAGS="$(CFLAGS)", + LIBFLAG="$(LIBFLAG)", + LUA_LIBDIR="$(LUA_LIBDIR)", + LUA_BINDIR="$(LUA_BINDIR)", + LUA_INCDIR="$(LUA_INCDIR)", + LUA="$(LUA)", + OPENSSL_INCDIR="$(OPENSSL_INCDIR)", + OPENSSL_LIBDIR="$(OPENSSL_LIBDIR)", + }, + install_variables = { + INST_PREFIX="$(PREFIX)", + INST_BINDIR="$(BINDIR)", + INST_LIBDIR="$(LIBDIR)", + INST_LUADIR="$(LUADIR)", + INST_CONFDIR="$(CONFDIR)", + }, +} From ddb9cd28bf33799652d252df0546aa0832933bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=85=E8=BF=9B=E8=B6=85?= Date: Sun, 9 Jan 2022 23:59:32 +0800 Subject: [PATCH 259/260] feat(grpc-web): support gRPC-Web Proxy (#5964) --- apisix/plugins/grpc-web.lua | 147 ++++ ci/centos7-ci.sh | 9 + ci/common.sh | 12 + ci/linux_openresty_common_runner.sh | 9 + conf/config-default.yaml | 1 + docs/en/latest/config.json | 1 + docs/en/latest/plugins/grpc-web.md | 85 +++ docs/zh/latest/config.json | 1 + docs/zh/latest/plugins/grpc-web.md | 84 +++ t/admin/plugins.t | 1 + t/plugin/grpc-web.t | 221 ++++++ t/plugin/grpc-web/a6/routes.pb.go | 513 ++++++++++++++ t/plugin/grpc-web/a6/routes.proto | 49 ++ t/plugin/grpc-web/a6/routes_grpc.pb.go | 97 +++ t/plugin/grpc-web/a6/routes_grpc_web_pb.js | 443 ++++++++++++ t/plugin/grpc-web/a6/routes_pb.js | 766 +++++++++++++++++++++ t/plugin/grpc-web/client.js | 178 +++++ t/plugin/grpc-web/go.mod | 13 + t/plugin/grpc-web/go.sum | 125 ++++ t/plugin/grpc-web/package-lock.json | 52 ++ t/plugin/grpc-web/package.json | 8 + t/plugin/grpc-web/server.go | 53 ++ t/plugin/grpc-web/setup.sh | 25 + 23 files changed, 2893 insertions(+) create mode 100644 apisix/plugins/grpc-web.lua create mode 100644 docs/en/latest/plugins/grpc-web.md create mode 100644 docs/zh/latest/plugins/grpc-web.md create mode 100644 t/plugin/grpc-web.t create mode 100644 t/plugin/grpc-web/a6/routes.pb.go create mode 100644 t/plugin/grpc-web/a6/routes.proto create mode 100644 t/plugin/grpc-web/a6/routes_grpc.pb.go create mode 100644 t/plugin/grpc-web/a6/routes_grpc_web_pb.js create mode 100644 t/plugin/grpc-web/a6/routes_pb.js create mode 100644 t/plugin/grpc-web/client.js create mode 100644 t/plugin/grpc-web/go.mod create mode 100644 t/plugin/grpc-web/go.sum create mode 100644 t/plugin/grpc-web/package-lock.json create mode 100644 t/plugin/grpc-web/package.json create mode 100644 t/plugin/grpc-web/server.go create mode 100755 t/plugin/grpc-web/setup.sh diff --git a/apisix/plugins/grpc-web.lua b/apisix/plugins/grpc-web.lua new file mode 100644 index 000000000000..428b641ce74c --- /dev/null +++ b/apisix/plugins/grpc-web.lua @@ -0,0 +1,147 @@ +-- +-- 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 ngx = ngx +local ngx_arg = ngx.arg +local core = require("apisix.core") +local req_set_uri = ngx.req.set_uri +local req_set_body_data = ngx.req.set_body_data +local decode_base64 = ngx.decode_base64 +local encode_base64 = ngx.encode_base64 + + +local ALLOW_METHOD_OPTIONS = "OPTIONS" +local ALLOW_METHOD_POST = "POST" +local CONTENT_ENCODING_BASE64 = "base64" +local CONTENT_ENCODING_BINARY = "binary" +local DEFAULT_CORS_CONTENT_TYPE = "application/grpc-web-text+proto" +local DEFAULT_CORS_ALLOW_ORIGIN = "*" +local DEFAULT_CORS_ALLOW_METHODS = ALLOW_METHOD_POST +local DEFAULT_CORS_ALLOW_HEADERS = "content-type,x-grpc-web,x-user-agent" +local DEFAULT_PROXY_CONTENT_TYPE = "application/grpc" + + +local plugin_name = "grpc-web" + +local schema = { + type = "object", + properties = {}, +} + +local grpc_web_content_encoding = { + ["application/grpc-web"] = CONTENT_ENCODING_BINARY, + ["application/grpc-web-text"] = CONTENT_ENCODING_BASE64, + ["application/grpc-web+proto"] = CONTENT_ENCODING_BINARY, + ["application/grpc-web-text+proto"] = CONTENT_ENCODING_BASE64, +} + +local _M = { + version = 0.1, + priority = 505, + name = plugin_name, + schema = schema, +} + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + +function _M.access(conf, ctx) + local method = core.request.get_method() + if method == ALLOW_METHOD_OPTIONS then + return 204 + end + + if method ~= ALLOW_METHOD_POST then + -- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support + core.log.error("request method: `", method, "` invalid") + return 400 + end + + local mimetype = core.request.header(ctx, "Content-Type") + local encoding = grpc_web_content_encoding[mimetype] + if not encoding then + core.log.error("request Content-Type: `", mimetype, "` invalid") + return 400 + end + + -- set grpc path + if not (ctx.curr_req_matched and ctx.curr_req_matched[":ext"]) then + core.log.error("routing configuration error, grpc-web plugin only supports ", + "`prefix matching` pattern routing") + return 400 + end + + local path = ctx.curr_req_matched[":ext"] + if path:byte(1) ~= core.string.byte("/") then + path = "/" .. path + end + + req_set_uri(path) + + -- set grpc body + local body, err = core.request.get_body() + if err then + core.log.error("failed to read request body, err: ", err) + return 400 + end + + if encoding == CONTENT_ENCODING_BASE64 then + body = decode_base64(body) + if not body then + core.log.error("failed to decode request body") + return 400 + end + end + + -- set grpc content-type + core.request.set_header(ctx, "Content-Type", DEFAULT_PROXY_CONTENT_TYPE) + -- set grpc body + req_set_body_data(body) + + -- set context variable + ctx.grpc_web_mime = mimetype + ctx.grpc_web_encoding = encoding +end + +function _M.header_filter(conf, ctx) + local method = core.request.get_method() + if method == ALLOW_METHOD_OPTIONS then + core.response.set_header("Access-Control-Allow-Methods", DEFAULT_CORS_ALLOW_METHODS) + core.response.set_header("Access-Control-Allow-Headers", DEFAULT_CORS_ALLOW_HEADERS) + end + core.response.set_header("Access-Control-Allow-Origin", DEFAULT_CORS_ALLOW_ORIGIN) + core.response.set_header("Content-Type", ctx.grpc_web_mime or DEFAULT_CORS_CONTENT_TYPE) +end + +function _M.body_filter(conf, ctx) + -- If the MIME extension type description of the gRPC-Web standard is not obtained, + -- indicating that the request is not based on the gRPC Web specification, + -- the processing of the request body will be ignored + -- https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md + -- https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support + if not ctx.grpc_web_mime then + return + end + + if ctx.grpc_web_encoding == CONTENT_ENCODING_BASE64 then + local chunk = ngx_arg[1] + chunk = encode_base64(chunk) + ngx_arg[1] = chunk + end +end + +return _M diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index 3167fcec6242..7c74eba9742e 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -75,6 +75,15 @@ install_dependencies() { # installing grpcurl install_grpcurl + # install nodejs + install_nodejs + + # grpc-web server && client + cd t/plugin/grpc-web + ./setup.sh + # back to home directory + cd ../../../ + # install dependencies git clone https://github.com/iresty/test-nginx.git test-nginx create_lua_deps diff --git a/ci/common.sh b/ci/common.sh index 51bee69a733a..fe015b16d666 100644 --- a/ci/common.sh +++ b/ci/common.sh @@ -62,4 +62,16 @@ install_vault_cli () { unzip vault_${VAULT_VERSION}_linux_amd64.zip && mv ./vault /usr/local/bin } +install_nodejs () { + NODEJS_PREFIX="/usr/local/node" + NODEJS_VERSION="16.13.1" + wget https://nodejs.org/dist/v${NODEJS_VERSION}/node-v${NODEJS_VERSION}-linux-x64.tar.xz + tar -xvf node-v${NODEJS_VERSION}-linux-x64.tar.xz + rm -f /usr/local/bin/node + rm -f /usr/local/bin/npm + mv node-v${NODEJS_VERSION}-linux-x64 ${NODEJS_PREFIX} + ln -s ${NODEJS_PREFIX}/bin/node /usr/local/bin/node + ln -s ${NODEJS_PREFIX}/bin/npm /usr/local/bin/npm +} + GRPC_SERVER_EXAMPLE_VER=20210819 diff --git a/ci/linux_openresty_common_runner.sh b/ci/linux_openresty_common_runner.sh index 9cfee16fad89..ced0b83ffdfe 100755 --- a/ci/linux_openresty_common_runner.sh +++ b/ci/linux_openresty_common_runner.sh @@ -57,6 +57,15 @@ do_install() { # install grpcurl install_grpcurl + # install nodejs + install_nodejs + + # grpc-web server && client + cd t/plugin/grpc-web + ./setup.sh + # back to home directory + cd ../../../ + # install vault cli capabilities install_vault_cli } diff --git a/conf/config-default.yaml b/conf/config-default.yaml index cbf97dd66931..e1ae17912921 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -362,6 +362,7 @@ plugins: # plugin list (sorted by priority) - response-rewrite # priority: 899 #- dubbo-proxy # priority: 507 - grpc-transcode # priority: 506 + - grpc-web # priority: 505 - prometheus # priority: 500 - datadog # priority: 495 - echo # priority: 412 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index dcbfe682cb0c..3f86445c0e5c 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -51,6 +51,7 @@ "plugins/response-rewrite", "plugins/proxy-rewrite", "plugins/grpc-transcode", + "plugins/grpc-web", "plugins/fault-injection" ] }, diff --git a/docs/en/latest/plugins/grpc-web.md b/docs/en/latest/plugins/grpc-web.md new file mode 100644 index 000000000000..17c01116f8ee --- /dev/null +++ b/docs/en/latest/plugins/grpc-web.md @@ -0,0 +1,85 @@ +--- +title: grpc-web +--- + + + +## Summary + +- [**Name**](#name) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +The `grpc-web` plugin is a proxy plugin used to process [gRPC Web](https://github.com/grpc/grpc-web) client requests to `gRPC Server`. + +gRPC Web Client -> APISIX -> gRPC server + +## How To Enable + +To enable the `gRPC Web` proxy plugin, routing must use the `Prefix matching` pattern (for example: `/*` or `/grpc/example/*`), +Because the `gRPC Web` client will pass the `package name`, `service interface name`, `method name` and other information declared in the `proto` in the URI (for example: `/path/a6.RouteService/Insert`) , +When using `Absolute Match`, it will not be able to hit the plugin and extract the `proto` information. + +```bash +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri":"/grpc/web/*", + "plugins":{ + "grpc-web":{} + }, + "upstream":{ + "scheme":"grpc", + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + } +}' +``` + +## Test Plugin + +- The request method only supports `POST` and `OPTIONS`, refer to: [CORS support](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support). +- The `Content-Type` supports `application/grpc-web`, `application/grpc-web-text`, `application/grpc-web+proto`, `application/grpc-web-text+proto`, refer to: [Protocol differences vs gRPC over HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2). +- Client deployment, refer to: [gRPC-Web Client Runtime Library](https://www.npmjs.com/package/grpc-web) or [Apache APISIX gRPC Web Test Framework](https://github.com/apache/apisix/tree/master/t/plugin/grpc-web). +- After the `gRPC Web` client is deployed, you can initiate a `gRPC Web` proxy request to `APISIX` through `browser` or `node`. + +## Disable Plugin + +Just delete the JSON configuration of `grpc-web` in the plugin configuration. +The APISIX plug-in is hot-reloaded, so there is no need to restart APISIX. + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri":"/grpc/web/*", + "plugins":{}, + "upstream":{ + "scheme":"grpc", + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + } +}' +``` diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 7ac810ca42d9..dbe2a00181c5 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -51,6 +51,7 @@ "plugins/response-rewrite", "plugins/proxy-rewrite", "plugins/grpc-transcode", + "plugins/grpc-web", "plugins/fault-injection" ] }, diff --git a/docs/zh/latest/plugins/grpc-web.md b/docs/zh/latest/plugins/grpc-web.md new file mode 100644 index 000000000000..206a931b4865 --- /dev/null +++ b/docs/zh/latest/plugins/grpc-web.md @@ -0,0 +1,84 @@ +--- +title: grpc-web +--- + + + +## 摘要 + +- [**定义**](#定义) +- [**如何开启**](#如何开启) +- [**测试插件**](#测试插件) +- [**禁用插件**](#禁用插件) + +## 定义 + +`grpc-web` 插件是一个代理插件,用于转换 [gRPC Web](https://github.com/grpc/grpc-web) 客户端到 `gRPC Server` 的请求。 + +gRPC Web Client -> APISIX -> gRPC server + +## 如何开启 + +启用 `gRPC Web` 代理插件,路由必须使用 `前缀匹配` 模式(例如:`/*` 或 `/grpc/example/*`), +因为 `gRPC Web` 客户端会在 URI 中传递 `proto` 中声明的`包名称`、`服务接口名称`、`方法名称`等信息(例如:`/path/a6.RouteService/Insert`), +使用 `绝对匹配` 时将无法命中插件和提取 `proto` 信息。 + +```bash +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri":"/grpc/web/*", + "plugins":{ + "grpc-web":{} + }, + "upstream":{ + "scheme":"grpc", + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + } +}' +``` + +## 测试插件 + +- 请求方式仅支持 `POST` 和 `OPTIONS`,参考:[CORS support](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md#cors-support) 。 +- 内容类型支持 `application/grpc-web`、`application/grpc-web-text`、`application/grpc-web+proto`、`application/grpc-web-text+proto`,参考:[Protocol differences vs gRPC over HTTP2](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2) 。 +- 客户端部署,参考:[gRPC-Web Client Runtime Library](https://www.npmjs.com/package/grpc-web) 或 [Apache APISIX gRPC Web 测试框架](https://github.com/apache/apisix/tree/master/t/plugin/grpc-web) 。 +- 完成 `gRPC Web` 客户端部署后,即可通过 `浏览器` 或 `node` 向 `APISIX` 发起 `gRPC Web` 代理请求。 + +## 禁用插件 + +只需删除插件配置中 `grpc-web` 的JSON配置即可。 APISIX 插件是热加载的,所以不需要重启 APISIX。 + +```bash +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri":"/grpc/web/*", + "plugins":{}, + "upstream":{ + "scheme":"grpc", + "type":"roundrobin", + "nodes":{ + "127.0.0.1:1980":1 + } + } +}' +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 2495b5395185..29038eaaba44 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -98,6 +98,7 @@ traffic-split redirect response-rewrite grpc-transcode +grpc-web prometheus datadog echo diff --git a/t/plugin/grpc-web.t b/t/plugin/grpc-web.t new file mode 100644 index 000000000000..37e1daccf2d4 --- /dev/null +++ b/t/plugin/grpc-web.t @@ -0,0 +1,221 @@ +# +# 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'; + +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: set route (default grpc web proxy route) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/grpc/*", + upstream = { + scheme = "grpc", + type = "roundrobin", + nodes = { + ["127.0.0.1:50001"] = 1 + } + }, + plugins = { + ["grpc-web"] = {} + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: Flush all data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js FLUSH +--- response_body +[] + + + +=== TEST 3: Insert first data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js POST 1 route01 path01 +--- response_body +[["1",{"name":"route01","path":"path01"}]] + + + +=== TEST 4: Update data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js PUT 1 route01 hello +--- response_body +[["1",{"name":"route01","path":"hello"}]] + + + +=== TEST 5: Insert second data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js POST 2 route02 path02 +--- response_body +[["1",{"name":"route01","path":"hello"}],["2",{"name":"route02","path":"path02"}]] + + + +=== TEST 6: Insert third data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js POST 3 route03 path03 +--- response_body +[["1",{"name":"route01","path":"hello"}],["2",{"name":"route02","path":"path02"}],["3",{"name":"route03","path":"path03"}]] + + + +=== TEST 7: Delete first data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js DEL 1 +--- response_body +[["2",{"name":"route02","path":"path02"}],["3",{"name":"route03","path":"path03"}]] + + + +=== TEST 8: Get second data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js GET 2 +--- response_body +{"name":"route02","path":"path02"} + + + +=== TEST 9: Get all data through APISIX gRPC-Web Proxy +--- exec +node ./t/plugin/grpc-web/client.js all +--- response_body +[["2",{"name":"route02","path":"path02"}],["3",{"name":"route03","path":"path03"}]] + + + +=== TEST 10: test options request +--- request +OPTIONS /grpc/a6.RouteService/GetAll +--- error_code: 204 +--- response_headers +Access-Control-Allow-Methods: POST +Access-Control-Allow-Headers: content-type,x-grpc-web,x-user-agent +Access-Control-Allow-Origin: * + + + +=== TEST 11: test non-options request +--- request +GET /grpc/a6.RouteService/GetAll +--- error_code: 400 +--- response_headers +Access-Control-Allow-Origin: * +Content-Type: application/grpc-web-text+proto +--- error_log +request method: `GET` invalid + + + +=== TEST 12: test non gRPC Web MIME type request +--- request +POST /grpc/a6.RouteService/GetAll +--- more_headers +Content-Type: application/json +--- error_code: 400 +--- response_headers +Access-Control-Allow-Origin: * +Content-Type: application/grpc-web-text+proto +--- error_log +request Content-Type: `application/json` invalid + + + +=== TEST 13: set route (absolute match) +--- config + location /t { + content_by_lua_block { + + local config = { + uri = "/grpc2/a6.RouteService/GetAll", + upstream = { + scheme = "grpc", + type = "roundrobin", + nodes = { + ["127.0.0.1:50001"] = 1 + } + }, + plugins = { + ["grpc-web"] = {} + } + } + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', ngx.HTTP_PUT, config) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 14: test route (absolute match) +--- request +POST /grpc2/a6.RouteService/GetAll +--- more_headers +Content-Type: application/grpc-web +--- error_code: 400 +--- response_headers +Access-Control-Allow-Origin: * +Content-Type: application/grpc-web-text+proto +--- error_log +routing configuration error, grpc-web plugin only supports `prefix matching` pattern routing diff --git a/t/plugin/grpc-web/a6/routes.pb.go b/t/plugin/grpc-web/a6/routes.pb.go new file mode 100644 index 000000000000..b00d62a7f07e --- /dev/null +++ b/t/plugin/grpc-web/a6/routes.pb.go @@ -0,0 +1,513 @@ +/* + * 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. + */ + +package a6 + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Empty struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Empty) Reset() { *m = Empty{} } +func (m *Empty) String() string { return proto.CompactTextString(m) } +func (*Empty) ProtoMessage() {} +func (*Empty) Descriptor() ([]byte, []int) { + return fileDescriptor_078f480fb67d0ab3, []int{0} +} + +func (m *Empty) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Empty.Unmarshal(m, b) +} +func (m *Empty) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Empty.Marshal(b, m, deterministic) +} +func (m *Empty) XXX_Merge(src proto.Message) { + xxx_messageInfo_Empty.Merge(m, src) +} +func (m *Empty) XXX_Size() int { + return xxx_messageInfo_Empty.Size(m) +} +func (m *Empty) XXX_DiscardUnknown() { + xxx_messageInfo_Empty.DiscardUnknown(m) +} + +var xxx_messageInfo_Empty proto.InternalMessageInfo + +type Route struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Route) Reset() { *m = Route{} } +func (m *Route) String() string { return proto.CompactTextString(m) } +func (*Route) ProtoMessage() {} +func (*Route) Descriptor() ([]byte, []int) { + return fileDescriptor_078f480fb67d0ab3, []int{1} +} + +func (m *Route) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Route.Unmarshal(m, b) +} +func (m *Route) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Route.Marshal(b, m, deterministic) +} +func (m *Route) XXX_Merge(src proto.Message) { + xxx_messageInfo_Route.Merge(m, src) +} +func (m *Route) XXX_Size() int { + return xxx_messageInfo_Route.Size(m) +} +func (m *Route) XXX_DiscardUnknown() { + xxx_messageInfo_Route.DiscardUnknown(m) +} + +var xxx_messageInfo_Route proto.InternalMessageInfo + +func (m *Route) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *Route) GetPath() string { + if m != nil { + return m.Path + } + return "" +} + +type Request struct { + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Route *Route `protobuf:"bytes,2,opt,name=route,proto3" json:"route,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} +func (*Request) Descriptor() ([]byte, []int) { + return fileDescriptor_078f480fb67d0ab3, []int{2} +} + +func (m *Request) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Request.Unmarshal(m, b) +} +func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Request.Marshal(b, m, deterministic) +} +func (m *Request) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request.Merge(m, src) +} +func (m *Request) XXX_Size() int { + return xxx_messageInfo_Request.Size(m) +} +func (m *Request) XXX_DiscardUnknown() { + xxx_messageInfo_Request.DiscardUnknown(m) +} + +var xxx_messageInfo_Request proto.InternalMessageInfo + +func (m *Request) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *Request) GetRoute() *Route { + if m != nil { + return m.Route + } + return nil +} + +type Response struct { + Status bool `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + Route *Route `protobuf:"bytes,2,opt,name=route,proto3" json:"route,omitempty"` + Routes map[string]*Route `protobuf:"bytes,3,rep,name=routes,proto3" json:"routes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Response) Reset() { *m = Response{} } +func (m *Response) String() string { return proto.CompactTextString(m) } +func (*Response) ProtoMessage() {} +func (*Response) Descriptor() ([]byte, []int) { + return fileDescriptor_078f480fb67d0ab3, []int{3} +} + +func (m *Response) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Response.Unmarshal(m, b) +} +func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Response.Marshal(b, m, deterministic) +} +func (m *Response) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response.Merge(m, src) +} +func (m *Response) XXX_Size() int { + return xxx_messageInfo_Response.Size(m) +} +func (m *Response) XXX_DiscardUnknown() { + xxx_messageInfo_Response.DiscardUnknown(m) +} + +var xxx_messageInfo_Response proto.InternalMessageInfo + +func (m *Response) GetStatus() bool { + if m != nil { + return m.Status + } + return false +} + +func (m *Response) GetRoute() *Route { + if m != nil { + return m.Route + } + return nil +} + +func (m *Response) GetRoutes() map[string]*Route { + if m != nil { + return m.Routes + } + return nil +} + +func init() { + proto.RegisterType((*Empty)(nil), "a6.Empty") + proto.RegisterType((*Route)(nil), "a6.Route") + proto.RegisterType((*Request)(nil), "a6.Request") + proto.RegisterType((*Response)(nil), "a6.Response") + proto.RegisterMapType((map[string]*Route)(nil), "a6.Response.RoutesEntry") +} + +func init() { proto.RegisterFile("routes.proto", fileDescriptor_078f480fb67d0ab3) } + +var fileDescriptor_078f480fb67d0ab3 = []byte{ + // 307 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0x4f, 0x4b, 0xf3, 0x40, + 0x10, 0x87, 0xdf, 0x24, 0x6f, 0xd2, 0x76, 0x52, 0x44, 0xe6, 0x20, 0xa1, 0x17, 0xcb, 0x4a, 0xa1, + 0xa7, 0x54, 0x2a, 0x14, 0xa9, 0x27, 0xc5, 0x5a, 0xbc, 0xae, 0x78, 0xf1, 0xb6, 0x9a, 0x81, 0x06, + 0xf3, 0xcf, 0xec, 0x26, 0x90, 0xcf, 0xe6, 0xc7, 0xf2, 0x0b, 0x48, 0x26, 0x39, 0x14, 0x14, 0x73, + 0x9b, 0x3c, 0x79, 0xe6, 0x37, 0x93, 0x21, 0x30, 0x2d, 0xf3, 0xca, 0x90, 0x0e, 0x8b, 0x32, 0x37, + 0x39, 0xda, 0x6a, 0x23, 0x46, 0xe0, 0xee, 0xd2, 0xc2, 0x34, 0x62, 0x05, 0xae, 0x6c, 0x5f, 0x22, + 0xc2, 0xff, 0x4c, 0xa5, 0x14, 0x58, 0x73, 0x6b, 0x39, 0x91, 0x5c, 0xb7, 0xac, 0x50, 0xe6, 0x10, + 0xd8, 0x1d, 0x6b, 0x6b, 0xb1, 0x85, 0x91, 0xa4, 0x8f, 0x8a, 0xb4, 0xc1, 0x13, 0xb0, 0xe3, 0xa8, + 0x6f, 0xb0, 0xe3, 0x08, 0xcf, 0xc1, 0xe5, 0x41, 0xec, 0xfb, 0xeb, 0x49, 0xa8, 0x36, 0x21, 0x87, + 0xcb, 0x8e, 0x8b, 0x4f, 0x0b, 0xc6, 0x92, 0x74, 0x91, 0x67, 0x9a, 0xf0, 0x0c, 0x3c, 0x6d, 0x94, + 0xa9, 0x34, 0x27, 0x8c, 0x65, 0xff, 0x34, 0x98, 0x82, 0x97, 0xe0, 0x75, 0xdf, 0x13, 0x38, 0x73, + 0x67, 0xe9, 0xaf, 0x03, 0x36, 0xfa, 0xd8, 0x4e, 0xd5, 0xbb, 0xcc, 0x94, 0x8d, 0xec, 0xbd, 0xd9, + 0x3d, 0xf8, 0x47, 0x18, 0x4f, 0xc1, 0x79, 0xa7, 0xa6, 0x5f, 0xbc, 0x2d, 0xdb, 0x99, 0xb5, 0x4a, + 0xaa, 0xdf, 0x66, 0x32, 0xdf, 0xda, 0xd7, 0xd6, 0xfa, 0xcb, 0x82, 0x29, 0xc3, 0x27, 0x2a, 0xeb, + 0xf8, 0x8d, 0x70, 0x01, 0xe3, 0x87, 0xa4, 0xd2, 0x87, 0xdb, 0x24, 0x41, 0x6e, 0xe1, 0x93, 0xce, + 0xa6, 0xc7, 0xfb, 0x88, 0x7f, 0x78, 0x01, 0xde, 0x9e, 0xcc, 0x80, 0x24, 0xc0, 0xd9, 0x93, 0x41, + 0xbf, 0xc3, 0x7c, 0xdf, 0x1f, 0xce, 0x02, 0xbc, 0xc7, 0x4c, 0x53, 0x39, 0xac, 0x3d, 0x17, 0x91, + 0x32, 0x34, 0xa8, 0x49, 0x4a, 0xf3, 0xfa, 0x6f, 0xed, 0x6e, 0xf4, 0xe2, 0x86, 0xab, 0x1b, 0xb5, + 0x79, 0xf5, 0xf8, 0xef, 0xb9, 0xfa, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xba, 0xd4, 0xb5, 0x13, 0x4d, + 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// RouteServiceClient is the client API for RouteService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type RouteServiceClient interface { + FlushAll(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Response, error) + GetAll(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Response, error) + Get(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) + Insert(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) + Update(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) + Remove(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) +} + +type routeServiceClient struct { + cc *grpc.ClientConn +} + +func NewRouteServiceClient(cc *grpc.ClientConn) RouteServiceClient { + return &routeServiceClient{cc} +} + +func (c *routeServiceClient) FlushAll(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/a6.RouteService/FlushAll", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routeServiceClient) GetAll(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/a6.RouteService/GetAll", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routeServiceClient) Get(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/a6.RouteService/Get", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routeServiceClient) Insert(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/a6.RouteService/Insert", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routeServiceClient) Update(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/a6.RouteService/Update", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *routeServiceClient) Remove(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/a6.RouteService/Remove", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RouteServiceServer is the server API for RouteService service. +type RouteServiceServer interface { + FlushAll(context.Context, *Empty) (*Response, error) + GetAll(context.Context, *Empty) (*Response, error) + Get(context.Context, *Request) (*Response, error) + Insert(context.Context, *Request) (*Response, error) + Update(context.Context, *Request) (*Response, error) + Remove(context.Context, *Request) (*Response, error) +} + +// UnimplementedRouteServiceServer can be embedded to have forward compatible implementations. +type UnimplementedRouteServiceServer struct { +} + +func (*UnimplementedRouteServiceServer) FlushAll(ctx context.Context, req *Empty) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method FlushAll not implemented") +} +func (*UnimplementedRouteServiceServer) GetAll(ctx context.Context, req *Empty) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAll not implemented") +} +func (*UnimplementedRouteServiceServer) Get(ctx context.Context, req *Request) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (*UnimplementedRouteServiceServer) Insert(ctx context.Context, req *Request) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Insert not implemented") +} +func (*UnimplementedRouteServiceServer) Update(ctx context.Context, req *Request) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (*UnimplementedRouteServiceServer) Remove(ctx context.Context, req *Request) (*Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Remove not implemented") +} + +func RegisterRouteServiceServer(s *grpc.Server, srv RouteServiceServer) { + s.RegisterService(&_RouteService_serviceDesc, srv) +} + +func _RouteService_FlushAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouteServiceServer).FlushAll(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/a6.RouteService/FlushAll", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouteServiceServer).FlushAll(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _RouteService_GetAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouteServiceServer).GetAll(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/a6.RouteService/GetAll", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouteServiceServer).GetAll(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _RouteService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouteServiceServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/a6.RouteService/Get", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouteServiceServer).Get(ctx, req.(*Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _RouteService_Insert_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouteServiceServer).Insert(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/a6.RouteService/Insert", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouteServiceServer).Insert(ctx, req.(*Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _RouteService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouteServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/a6.RouteService/Update", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouteServiceServer).Update(ctx, req.(*Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _RouteService_Remove_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouteServiceServer).Remove(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/a6.RouteService/Remove", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouteServiceServer).Remove(ctx, req.(*Request)) + } + return interceptor(ctx, in, info, handler) +} + +var _RouteService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "a6.RouteService", + HandlerType: (*RouteServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "FlushAll", + Handler: _RouteService_FlushAll_Handler, + }, + { + MethodName: "GetAll", + Handler: _RouteService_GetAll_Handler, + }, + { + MethodName: "Get", + Handler: _RouteService_Get_Handler, + }, + { + MethodName: "Insert", + Handler: _RouteService_Insert_Handler, + }, + { + MethodName: "Update", + Handler: _RouteService_Update_Handler, + }, + { + MethodName: "Remove", + Handler: _RouteService_Remove_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "routes.proto", +} diff --git a/t/plugin/grpc-web/a6/routes.proto b/t/plugin/grpc-web/a6/routes.proto new file mode 100644 index 000000000000..d8946c601a1f --- /dev/null +++ b/t/plugin/grpc-web/a6/routes.proto @@ -0,0 +1,49 @@ +// +// 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 a6; + +option go_package = "./;a6"; + +service RouteService { + rpc FlushAll (Empty) returns (Response) {} + rpc GetAll (Empty) returns (Response) {} + rpc Get (Request) returns (Response) {} + rpc Insert (Request) returns (Response) {} + rpc Update (Request) returns (Response) {} + rpc Remove (Request) returns (Response) {} +} + +message Empty {} + +message Route { + string name = 1; + string path = 2; +} + +message Request { + string id = 1; + Route route = 2; +} + +message Response { + bool status = 1; + Route route = 2; + map routes = 3; +} diff --git a/t/plugin/grpc-web/a6/routes_grpc.pb.go b/t/plugin/grpc-web/a6/routes_grpc.pb.go new file mode 100644 index 000000000000..e2bdab4cf50f --- /dev/null +++ b/t/plugin/grpc-web/a6/routes_grpc.pb.go @@ -0,0 +1,97 @@ +/* + * 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. + */ + +package a6 + +import ( + "errors" + "golang.org/x/net/context" + + uuid "github.com/satori/go.uuid" +) + +type RouteServer struct { + Routes map[string]*Route +} + +func (rs *RouteServer) init() { + if rs.Routes == nil { + rs.Routes = make(map[string]*Route) + } +} + +func (rs *RouteServer) FlushAll(ctx context.Context, req *Empty) (*Response, error) { + if rs.Routes != nil { + rs.Routes = make(map[string]*Route) + } + return &Response{Routes: rs.Routes, Status: true}, nil +} + +func (rs *RouteServer) GetAll(ctx context.Context, req *Empty) (*Response, error) { + rs.init() + return &Response{Routes: rs.Routes, Status: true}, nil +} + +func (rs *RouteServer) Get(ctx context.Context, req *Request) (*Response, error) { + rs.init() + if len(req.Id) == 0 { + return &Response{Status: false}, errors.New("route ID undefined") + } + + if route, ok := rs.Routes[req.Id]; ok { + return &Response{Status: true, Route: route}, nil + } + + return &Response{Status: false}, errors.New("route not found") +} + +func (rs *RouteServer) Insert(ctx context.Context, req *Request) (*Response, error) { + rs.init() + if len(req.Id) <= 0 { + req.Id = uuid.NewV4().String() + } + rs.Routes[req.Id] = req.Route + return &Response{Status: true, Routes: rs.Routes}, nil +} + +func (rs *RouteServer) Update(ctx context.Context, req *Request) (*Response, error) { + rs.init() + if len(req.Id) == 0 { + return &Response{Status: false}, errors.New("route ID undefined") + } + + if _, ok := rs.Routes[req.Id]; ok { + rs.Routes[req.Id] = req.Route + return &Response{Status: true, Routes: rs.Routes}, nil + } + + return &Response{Status: false}, errors.New("route not found") +} + +func (rs *RouteServer) Remove(ctx context.Context, req *Request) (*Response, error) { + rs.init() + if len(req.Id) == 0 { + return &Response{Status: false}, errors.New("route ID undefined") + } + + if _, ok := rs.Routes[req.Id]; ok { + delete(rs.Routes, req.Id) + return &Response{Status: true, Routes: rs.Routes}, nil + } + + return &Response{Status: false}, errors.New("route not found") +} diff --git a/t/plugin/grpc-web/a6/routes_grpc_web_pb.js b/t/plugin/grpc-web/a6/routes_grpc_web_pb.js new file mode 100644 index 000000000000..fff570e14824 --- /dev/null +++ b/t/plugin/grpc-web/a6/routes_grpc_web_pb.js @@ -0,0 +1,443 @@ +/* + * 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. + */ + +const grpc = {}; +grpc.web = require('grpc-web'); + +const proto = {}; +proto.a6 = require('./routes_pb.js'); + +/** + * @param {string} hostname + * @param {?Object} credentials + * @param {?grpc.web.ClientOptions} options + * @constructor + * @struct + * @final + */ +proto.a6.RouteServiceClient = + function(hostname, credentials, options) { + if (!options) options = {}; + options.format = 'binary'; + + /** + * @private @const {!grpc.web.GrpcWebClientBase} The client + */ + this.client_ = new grpc.web.GrpcWebClientBase(options); + + /** + * @private @const {string} The hostname + */ + this.hostname_ = hostname; + +}; + + +/** + * @param {string} hostname + * @param {?Object} credentials + * @param {?grpc.web.ClientOptions} options + * @constructor + * @struct + * @final + */ +proto.a6.RouteServicePromiseClient = + function(hostname, credentials, options) { + if (!options) options = {}; + options.format = 'binary'; + + /** + * @private @const {!grpc.web.GrpcWebClientBase} The client + */ + this.client_ = new grpc.web.GrpcWebClientBase(options); + + /** + * @private @const {string} The hostname + */ + this.hostname_ = hostname; + +}; + + +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.a6.Empty, + * !proto.a6.Response>} + */ +const methodDescriptor_RouteService_FlushAll = new grpc.web.MethodDescriptor( + '/a6.RouteService/FlushAll', + grpc.web.MethodType.UNARY, + proto.a6.Empty, + proto.a6.Response, + /** + * @param {!proto.a6.Empty} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.a6.Response.deserializeBinary +); + + +/** + * @param {!proto.a6.Empty} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.a6.Response)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.a6.RouteServiceClient.prototype.flushAll = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/a6.RouteService/FlushAll', + request, + metadata || {}, + methodDescriptor_RouteService_FlushAll, + callback); +}; + + +/** + * @param {!proto.a6.Empty} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.a6.RouteServicePromiseClient.prototype.flushAll = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/a6.RouteService/FlushAll', + request, + metadata || {}, + methodDescriptor_RouteService_FlushAll); +}; + + +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.a6.Empty, + * !proto.a6.Response>} + */ +const methodDescriptor_RouteService_GetAll = new grpc.web.MethodDescriptor( + '/a6.RouteService/GetAll', + grpc.web.MethodType.UNARY, + proto.a6.Empty, + proto.a6.Response, + /** + * @param {!proto.a6.Empty} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.a6.Response.deserializeBinary +); + + +/** + * @param {!proto.a6.Empty} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.a6.Response)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.a6.RouteServiceClient.prototype.getAll = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/a6.RouteService/GetAll', + request, + metadata || {}, + methodDescriptor_RouteService_GetAll, + callback); +}; + + +/** + * @param {!proto.a6.Empty} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.a6.RouteServicePromiseClient.prototype.getAll = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/a6.RouteService/GetAll', + request, + metadata || {}, + methodDescriptor_RouteService_GetAll); +}; + + +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.a6.Request, + * !proto.a6.Response>} + */ +const methodDescriptor_RouteService_Get = new grpc.web.MethodDescriptor( + '/a6.RouteService/Get', + grpc.web.MethodType.UNARY, + proto.a6.Request, + proto.a6.Response, + /** + * @param {!proto.a6.Request} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.a6.Response.deserializeBinary +); + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.a6.Response)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.a6.RouteServiceClient.prototype.get = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/a6.RouteService/Get', + request, + metadata || {}, + methodDescriptor_RouteService_Get, + callback); +}; + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.a6.RouteServicePromiseClient.prototype.get = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/a6.RouteService/Get', + request, + metadata || {}, + methodDescriptor_RouteService_Get); +}; + + +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.a6.Request, + * !proto.a6.Response>} + */ +const methodDescriptor_RouteService_Insert = new grpc.web.MethodDescriptor( + '/a6.RouteService/Insert', + grpc.web.MethodType.UNARY, + proto.a6.Request, + proto.a6.Response, + /** + * @param {!proto.a6.Request} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.a6.Response.deserializeBinary +); + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.a6.Response)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.a6.RouteServiceClient.prototype.insert = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/a6.RouteService/Insert', + request, + metadata || {}, + methodDescriptor_RouteService_Insert, + callback); +}; + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.a6.RouteServicePromiseClient.prototype.insert = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/a6.RouteService/Insert', + request, + metadata || {}, + methodDescriptor_RouteService_Insert); +}; + + +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.a6.Request, + * !proto.a6.Response>} + */ +const methodDescriptor_RouteService_Update = new grpc.web.MethodDescriptor( + '/a6.RouteService/Update', + grpc.web.MethodType.UNARY, + proto.a6.Request, + proto.a6.Response, + /** + * @param {!proto.a6.Request} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.a6.Response.deserializeBinary +); + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.a6.Response)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.a6.RouteServiceClient.prototype.update = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/a6.RouteService/Update', + request, + metadata || {}, + methodDescriptor_RouteService_Update, + callback); +}; + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.a6.RouteServicePromiseClient.prototype.update = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/a6.RouteService/Update', + request, + metadata || {}, + methodDescriptor_RouteService_Update); +}; + + +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.a6.Request, + * !proto.a6.Response>} + */ +const methodDescriptor_RouteService_Remove = new grpc.web.MethodDescriptor( + '/a6.RouteService/Remove', + grpc.web.MethodType.UNARY, + proto.a6.Request, + proto.a6.Response, + /** + * @param {!proto.a6.Request} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + proto.a6.Response.deserializeBinary +); + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.a6.Response)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.a6.RouteServiceClient.prototype.remove = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/a6.RouteService/Remove', + request, + metadata || {}, + methodDescriptor_RouteService_Remove, + callback); +}; + + +/** + * @param {!proto.a6.Request} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.a6.RouteServicePromiseClient.prototype.remove = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/a6.RouteService/Remove', + request, + metadata || {}, + methodDescriptor_RouteService_Remove); +}; + + +module.exports = proto.a6; + diff --git a/t/plugin/grpc-web/a6/routes_pb.js b/t/plugin/grpc-web/a6/routes_pb.js new file mode 100644 index 000000000000..90612d3d6125 --- /dev/null +++ b/t/plugin/grpc-web/a6/routes_pb.js @@ -0,0 +1,766 @@ +/* + * 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. + */ + +var jspb = require('google-protobuf'); +var goog = jspb; +var global = Function('return this')(); + +goog.exportSymbol('proto.a6.Empty', null, global); +goog.exportSymbol('proto.a6.Request', null, global); +goog.exportSymbol('proto.a6.Response', null, global); +goog.exportSymbol('proto.a6.Route', null, global); +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.a6.Empty = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.a6.Empty, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.a6.Empty.displayName = 'proto.a6.Empty'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.a6.Route = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.a6.Route, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.a6.Route.displayName = 'proto.a6.Route'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.a6.Request = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.a6.Request, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.a6.Request.displayName = 'proto.a6.Request'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.a6.Response = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.a6.Response, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.a6.Response.displayName = 'proto.a6.Response'; +} + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.a6.Empty.prototype.toObject = function(opt_includeInstance) { + return proto.a6.Empty.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.a6.Empty} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Empty.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.a6.Empty} + */ +proto.a6.Empty.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.a6.Empty; + return proto.a6.Empty.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.a6.Empty} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.a6.Empty} + */ +proto.a6.Empty.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.a6.Empty.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.a6.Empty.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.a6.Empty} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Empty.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.a6.Route.prototype.toObject = function(opt_includeInstance) { + return proto.a6.Route.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.a6.Route} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Route.toObject = function(includeInstance, msg) { + var f, obj = { + name: jspb.Message.getFieldWithDefault(msg, 1, ""), + path: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.a6.Route} + */ +proto.a6.Route.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.a6.Route; + return proto.a6.Route.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.a6.Route} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.a6.Route} + */ +proto.a6.Route.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setName(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setPath(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.a6.Route.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.a6.Route.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.a6.Route} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Route.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getName(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getPath(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } +}; + + +/** + * optional string name = 1; + * @return {string} + */ +proto.a6.Route.prototype.getName = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.a6.Route} returns this + */ +proto.a6.Route.prototype.setName = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string path = 2; + * @return {string} + */ +proto.a6.Route.prototype.getPath = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.a6.Route} returns this + */ +proto.a6.Route.prototype.setPath = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.a6.Request.prototype.toObject = function(opt_includeInstance) { + return proto.a6.Request.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.a6.Request} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Request.toObject = function(includeInstance, msg) { + var f, obj = { + id: jspb.Message.getFieldWithDefault(msg, 1, ""), + route: (f = msg.getRoute()) && proto.a6.Route.toObject(includeInstance, f) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.a6.Request} + */ +proto.a6.Request.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.a6.Request; + return proto.a6.Request.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.a6.Request} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.a6.Request} + */ +proto.a6.Request.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setId(value); + break; + case 2: + var value = new proto.a6.Route; + reader.readMessage(value,proto.a6.Route.deserializeBinaryFromReader); + msg.setRoute(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.a6.Request.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.a6.Request.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.a6.Request} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Request.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getRoute(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.a6.Route.serializeBinaryToWriter + ); + } +}; + + +/** + * optional string id = 1; + * @return {string} + */ +proto.a6.Request.prototype.getId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.a6.Request} returns this + */ +proto.a6.Request.prototype.setId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional Route route = 2; + * @return {?proto.a6.Route} + */ +proto.a6.Request.prototype.getRoute = function() { + return /** @type{?proto.a6.Route} */ ( + jspb.Message.getWrapperField(this, proto.a6.Route, 2)); +}; + + +/** + * @param {?proto.a6.Route|undefined} value + * @return {!proto.a6.Request} returns this +*/ +proto.a6.Request.prototype.setRoute = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.a6.Request} returns this + */ +proto.a6.Request.prototype.clearRoute = function() { + return this.setRoute(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.a6.Request.prototype.hasRoute = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.a6.Response.prototype.toObject = function(opt_includeInstance) { + return proto.a6.Response.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.a6.Response} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Response.toObject = function(includeInstance, msg) { + var f, obj = { + status: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + route: (f = msg.getRoute()) && proto.a6.Route.toObject(includeInstance, f), + routesMap: (f = msg.getRoutesMap()) ? f.toObject(includeInstance, proto.a6.Route.toObject) : [] + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.a6.Response} + */ +proto.a6.Response.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.a6.Response; + return proto.a6.Response.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.a6.Response} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.a6.Response} + */ +proto.a6.Response.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setStatus(value); + break; + case 2: + var value = new proto.a6.Route; + reader.readMessage(value,proto.a6.Route.deserializeBinaryFromReader); + msg.setRoute(value); + break; + case 3: + var value = msg.getRoutesMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readString, jspb.BinaryReader.prototype.readMessage, proto.a6.Route.deserializeBinaryFromReader, "", new proto.a6.Route()); + }); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.a6.Response.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.a6.Response.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.a6.Response} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.a6.Response.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getStatus(); + if (f) { + writer.writeBool( + 1, + f + ); + } + f = message.getRoute(); + if (f != null) { + writer.writeMessage( + 2, + f, + proto.a6.Route.serializeBinaryToWriter + ); + } + f = message.getRoutesMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(3, writer, jspb.BinaryWriter.prototype.writeString, jspb.BinaryWriter.prototype.writeMessage, proto.a6.Route.serializeBinaryToWriter); + } +}; + + +/** + * optional bool status = 1; + * @return {boolean} + */ +proto.a6.Response.prototype.getStatus = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.a6.Response} returns this + */ +proto.a6.Response.prototype.setStatus = function(value) { + return jspb.Message.setProto3BooleanField(this, 1, value); +}; + + +/** + * optional Route route = 2; + * @return {?proto.a6.Route} + */ +proto.a6.Response.prototype.getRoute = function() { + return /** @type{?proto.a6.Route} */ ( + jspb.Message.getWrapperField(this, proto.a6.Route, 2)); +}; + + +/** + * @param {?proto.a6.Route|undefined} value + * @return {!proto.a6.Response} returns this +*/ +proto.a6.Response.prototype.setRoute = function(value) { + return jspb.Message.setWrapperField(this, 2, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.a6.Response} returns this + */ +proto.a6.Response.prototype.clearRoute = function() { + return this.setRoute(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.a6.Response.prototype.hasRoute = function() { + return jspb.Message.getField(this, 2) != null; +}; + + +/** + * map routes = 3; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.a6.Response.prototype.getRoutesMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 3, opt_noLazyCreate, + proto.a6.Route)); +}; + + +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.a6.Response} returns this + */ +proto.a6.Response.prototype.clearRoutesMap = function() { + this.getRoutesMap().clear(); + return this;}; + + +goog.object.extend(exports, proto.a6); diff --git a/t/plugin/grpc-web/client.js b/t/plugin/grpc-web/client.js new file mode 100644 index 000000000000..7f37ff06fac1 --- /dev/null +++ b/t/plugin/grpc-web/client.js @@ -0,0 +1,178 @@ +/* + * 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. + */ + +global.XMLHttpRequest = require('xhr2') + +const {Empty, Request, Route} = require('./a6/routes_pb') +const {RouteServiceClient} = require('./a6/routes_grpc_web_pb') + +const FUNCTION_ALL = "ALL" +const FUNCTION_GET = "GET" +const FUNCTION_POST = "POST" +const FUNCTION_PUT = "PUT" +const FUNCTION_DEL = "DEL" +const FUNCTION_FLUSH = "FLUSH" + +const functions = [FUNCTION_ALL, FUNCTION_GET, FUNCTION_POST, FUNCTION_PUT, FUNCTION_DEL, FUNCTION_FLUSH] + +class gRPCWebClient { + constructor() { + this.client = new RouteServiceClient("http://127.0.0.1:1984/grpc", null, null) + }; + + flush() { + let request = new Empty() + this.client.flushAll(request, {}, function (error, response) { + if (error) { + console.log(error) + return + } + console.log(JSON.stringify(response.toObject().routesMap)) + }); + } + + all() { + let request = new Empty() + this.client.getAll(request, {}, function (error, response) { + if (error) { + console.log(error) + return + } + console.log(JSON.stringify(response.toObject().routesMap)) + }); + } + + get(params) { + if (params[0] === null) { + console.log("route ID invalid") + return + } + let request = new Request() + request.setId(params[0]) + this.client.get(request, {}, function (error, response) { + if (error) { + console.log(error) + return + } + console.log(JSON.stringify(response.toObject().route)) + }); + } + + post(params) { + if (params[0] === null) { + console.log("route ID invalid") + return + } + if (params[1] === null) { + console.log("route Name invalid") + return + } + if (params[2] === null) { + console.log("route Path invalid") + return + } + let request = new Request() + let route = new Route() + request.setId(params[0]) + route.setName(params[1]) + route.setPath(params[2]) + request.setRoute(route) + this.client.insert(request, {}, function (error, response) { + if (error) { + console.log(error) + return + } + console.log(JSON.stringify(response.toObject().routesMap)) + }); + } + + put(params) { + if (params[0] === null) { + console.log("route ID invalid") + return + } + if (params[1] === null) { + console.log("route Name invalid") + return + } + if (params[2] === null) { + console.log("route Path invalid") + return + } + let request = new Request() + let route = new Route() + request.setId(params[0]) + route.setName(params[1]) + route.setPath(params[2]) + request.setRoute(route) + this.client.update(request, {}, function (error, response) { + if (error) { + console.log(error) + return + } + console.log(JSON.stringify(response.toObject().routesMap)) + }) + } + + del() { + if (params[0] === null) { + console.log("route ID invalid") + return + } + let request = new Request() + request.setId(params[0]) + this.client.remove(request, {}, function (error, response) { + if (error) { + console.log(error) + return + } + console.log(JSON.stringify(response.toObject().routesMap)) + }) + } +} + + +const arguments = process.argv.splice(2) + +if (arguments.length === 0) { + console.log("please input dispatch function, e.g: node client.js insert arg_id arg_name arg_path") + return +} + +const func = arguments[0].toUpperCase() +if (!functions.includes(func)) { + console.log("dispatch function not found") + return +} + +const params = arguments.splice(1) + +let grpc = new gRPCWebClient(); + +if (func === FUNCTION_GET) { + grpc.get(params) +} else if (func === FUNCTION_POST) { + grpc.post(params) +} else if (func === FUNCTION_PUT) { + grpc.put(params) +} else if (func === FUNCTION_DEL) { + grpc.del(params) +} else if (func === FUNCTION_FLUSH) { + grpc.flush() +} else { + grpc.all() +} diff --git a/t/plugin/grpc-web/go.mod b/t/plugin/grpc-web/go.mod new file mode 100644 index 000000000000..cece5f1ddd73 --- /dev/null +++ b/t/plugin/grpc-web/go.mod @@ -0,0 +1,13 @@ +module apisix.apache.org/plugin/grpc-web + +go 1.16 + +require ( + github.com/golang/protobuf v1.4.3 + github.com/satori/go.uuid v1.2.0 // indirect + golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect + golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/text v0.3.6 // indirect + google.golang.org/grpc v1.43.0 +) diff --git a/t/plugin/grpc-web/go.sum b/t/plugin/grpc-web/go.sum new file mode 100644 index 000000000000..626dadfd33c9 --- /dev/null +++ b/t/plugin/grpc-web/go.sum @@ -0,0 +1,125 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= +google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/t/plugin/grpc-web/package-lock.json b/t/plugin/grpc-web/package-lock.json new file mode 100644 index 000000000000..dade6174117c --- /dev/null +++ b/t/plugin/grpc-web/package-lock.json @@ -0,0 +1,52 @@ +{ + "name": "apisix-grpc-web", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "apisix.apache.org/plugin/grpc-web", + "dependencies": { + "google-protobuf": "^3.19.1", + "grpc-web": "^1.3.0", + "xhr2": "^0.2.1" + } + }, + "node_modules/google-protobuf": { + "version": "3.19.1", + "resolved": "https://registry.npmmirror.com/google-protobuf/download/google-protobuf-3.19.1.tgz?cache=0&sync_timestamp=1635869461201&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fgoogle-protobuf%2Fdownload%2Fgoogle-protobuf-3.19.1.tgz", + "integrity": "sha1-WvU5DoIGxEbY9J/rr/1Lf0rCj0E=", + "license": "BSD-3-Clause" + }, + "node_modules/grpc-web": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/grpc-web/download/grpc-web-1.3.0.tgz", + "integrity": "sha1-TDbZfnp7YQKn30Y+eCLNhtT2Xtg=", + "license": "Apache-2.0" + }, + "node_modules/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npm.taobao.org/xhr2/download/xhr2-0.2.1.tgz", + "integrity": "sha1-TnOtxPnP7Jy9IVf3Pv3OOl8QipM=", + "engines": { + "node": ">= 6" + } + } + }, + "dependencies": { + "google-protobuf": { + "version": "3.19.1", + "resolved": "https://registry.npmmirror.com/google-protobuf/download/google-protobuf-3.19.1.tgz?cache=0&sync_timestamp=1635869461201&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fgoogle-protobuf%2Fdownload%2Fgoogle-protobuf-3.19.1.tgz", + "integrity": "sha1-WvU5DoIGxEbY9J/rr/1Lf0rCj0E=" + }, + "grpc-web": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/grpc-web/download/grpc-web-1.3.0.tgz", + "integrity": "sha1-TDbZfnp7YQKn30Y+eCLNhtT2Xtg=" + }, + "xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npm.taobao.org/xhr2/download/xhr2-0.2.1.tgz", + "integrity": "sha1-TnOtxPnP7Jy9IVf3Pv3OOl8QipM=" + } + } +} diff --git a/t/plugin/grpc-web/package.json b/t/plugin/grpc-web/package.json new file mode 100644 index 000000000000..29b035cd167b --- /dev/null +++ b/t/plugin/grpc-web/package.json @@ -0,0 +1,8 @@ +{ + "name": "apisix-grpc-web", + "dependencies": { + "google-protobuf": "^3.19.1", + "grpc-web": "^1.3.0", + "xhr2": "^0.2.1" + } +} diff --git a/t/plugin/grpc-web/server.go b/t/plugin/grpc-web/server.go new file mode 100644 index 000000000000..100b95796e51 --- /dev/null +++ b/t/plugin/grpc-web/server.go @@ -0,0 +1,53 @@ +/* + * 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. + */ + +package main + +import ( + "flag" + "log" + "net" + + "apisix.apache.org/plugin/grpc-web/a6" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var grpcListenAddress string + +func init() { + flag.StringVar(&grpcListenAddress, "listen", ":50001", "address for grpc") +} + +func main() { + flag.Parse() + listen, err := net.Listen("tcp", grpcListenAddress) + if err != nil { + log.Fatalf("failed to listen gRPC-Web Test Server: %v", err) + } else { + log.Printf("successful to listen gRPC-Web Test Server, address %s", grpcListenAddress) + } + + s := a6.RouteServer{} + grpcServer := grpc.NewServer() + reflection.Register(grpcServer) + a6.RegisterRouteServiceServer(grpcServer, &s) + + if err = grpcServer.Serve(listen); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/t/plugin/grpc-web/setup.sh b/t/plugin/grpc-web/setup.sh new file mode 100755 index 000000000000..4305ee49aca5 --- /dev/null +++ b/t/plugin/grpc-web/setup.sh @@ -0,0 +1,25 @@ +#!/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. +# + +set -ex + +npm install + +CGO_ENABLED=0 go build -o grpc-web-server server.go + +./grpc-web-server > grpc-web-server.log 2>&1 || (cat grpc-web-server.log && exit 1)& From a1deeef71ce57b3289698b2d3dcca82ff2b62b82 Mon Sep 17 00:00:00 2001 From: mango <35127166+mangoGoForward@users.noreply.github.com> Date: Fri, 14 Jan 2022 09:33:12 +0800 Subject: [PATCH 260/260] Revert "feat: support hide the authentication header in basic-auth" --- apisix/discovery/eureka/init.lua | 12 +++- apisix/plugins/basic-auth.lua | 16 +---- conf/config-default.yaml | 2 +- docs/en/latest/plugins/basic-auth.md | 13 ++-- docs/zh/latest/plugins/basic-auth.md | 1 - t/plugin/basic-auth.t | 100 --------------------------- 6 files changed, 17 insertions(+), 127 deletions(-) diff --git a/apisix/discovery/eureka/init.lua b/apisix/discovery/eureka/init.lua index df72a5269e59..481e8e4b212a 100644 --- a/apisix/discovery/eureka/init.lua +++ b/apisix/discovery/eureka/init.lua @@ -19,6 +19,7 @@ local local_conf = require("apisix.core.config_local").local_conf() local http = require("resty.http") local core = require("apisix.core") local ipmatcher = require("resty.ipmatcher") +local zlib = require("zlib") local ipairs = ipairs local tostring = tostring local type = type @@ -161,10 +162,15 @@ local function fetch_full_registry(premature) return end - local json_str = res.body - local data, err = core.json.decode(json_str) + local encoding = res.headers["Content-Encoding"] + local res_body = res.body + if encoding == 'gzip' then + local stream = zlib.inflate() + res_body = stream(res_body) + end + local data, decode_err = core.json.decode(res_body) if not data then - log.error("invalid response body: ", json_str, " err: ", err) + log.error("invalid response body: ", res_body, " err: ", decode_err) return end local apps = data.applications.application diff --git a/apisix/plugins/basic-auth.lua b/apisix/plugins/basic-auth.lua index 25183899f519..5e780566310e 100644 --- a/apisix/plugins/basic-auth.lua +++ b/apisix/plugins/basic-auth.lua @@ -30,12 +30,7 @@ local consumers_lrucache = core.lrucache.new({ local schema = { type = "object", title = "work with route or service object", - properties = { - hide_auth_header = { - type = "boolean", - default = true, - } - }, + properties = {}, } local consumer_schema = { @@ -44,10 +39,6 @@ local consumer_schema = { properties = { username = { type = "string" }, password = { type = "string" }, - hide_auth_header = { - type = "boolean", - default = true, - } }, required = {"username", "password"}, } @@ -181,11 +172,6 @@ function _M.rewrite(conf, ctx) return 401, { message = "Password is error" } end - -- 5. hide `Authentication` header if `hide_auth_header` is `true` - if conf.hide_auth_header == true then - core.response.set_header("Authentication", "") - end - consumer.attach_consumer(ctx, cur_consumer, consumer_conf) core.log.info("hit basic-auth access") diff --git a/conf/config-default.yaml b/conf/config-default.yaml index e1ae17912921..c0f8837ff912 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -306,7 +306,7 @@ etcd: # eureka: # host: # it's possible to define multiple eureka hosts addresses of the same eureka cluster. # - "http://127.0.0.1:8761" -# prefix: /eureka/ +# prefix: /eureka/v2/ # fetch_interval: 30 # default 30s # weight: 100 # default weight for node # timeout: diff --git a/docs/en/latest/plugins/basic-auth.md b/docs/en/latest/plugins/basic-auth.md index 9f7ce861db65..e618a58a8500 100644 --- a/docs/en/latest/plugins/basic-auth.md +++ b/docs/en/latest/plugins/basic-auth.md @@ -39,11 +39,10 @@ For more information on Basic authentication, refer to [Wiki](https://en.wikiped ## Attributes -| Name | Type | Requirement | Default | Valid | Description | -| -------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| username | string | required | | | Different `consumer` should have different value which is unique. When different `consumer` use a same `username`, a request matching exception would be raised. | -| password | string | required | | | the user's password | -| hide_auth_header | boolean | optional | true | | Whether to return the Authentication response headers to the client. | +| Name | Type | Requirement | Default | Valid | Description | +| -------- | ------ | ----------- | ------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| username | string | required | | | Different `consumer` should have different value which is unique. When different `consumer` use a same `username`, a request matching exception would be raised. | +| password | string | required | | | the user's password | ## How To Enable @@ -130,8 +129,8 @@ hello, world ## Disable Plugin When you want to disable the `basic-auth` plugin, it is very simple, -you can delete the corresponding json configuration in the plugin configuration, -no need to restart the service, it will take effect immediately: + you can delete the corresponding json configuration in the plugin configuration, + no need to restart the service, it will take effect immediately: ```shell $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -X PUT -d ' diff --git a/docs/zh/latest/plugins/basic-auth.md b/docs/zh/latest/plugins/basic-auth.md index f7715bef2a4f..667721bd7ede 100644 --- a/docs/zh/latest/plugins/basic-auth.md +++ b/docs/zh/latest/plugins/basic-auth.md @@ -43,7 +43,6 @@ title: basic-auth | -------- | ------ | ------ | ------ | ------ | ------------------------------------------------------------------------------------------------------------------ | | username | string | 必须 | | | 不同的 `consumer` 对象应有不同的值,它应当是唯一的。不同 consumer 使用了相同的 `username` ,将会出现请求匹配异常。 | | password | string | 必须 | | | 用户的密码 | -| hide_auth_header | boolean | 可选 | true | | 是否将 Authentication 响应头返回给客户端. | ## 如何启用 diff --git a/t/plugin/basic-auth.t b/t/plugin/basic-auth.t index 5c06e2ca7e83..a780f3b618f8 100644 --- a/t/plugin/basic-auth.t +++ b/t/plugin/basic-auth.t @@ -395,103 +395,3 @@ GET /t GET /t --- no_error_log [error] - - - -=== TEST 15: hide auth header = false ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/consumers', - ngx.HTTP_PUT, - [[{ - "username": "foo", - "plugins": { - "basic-auth": { - "username": "foo", - "password": "bar", - "hide_auth_header": false - } - } - }]], - [[{ - "node": { - "value": { - "username": "foo", - "plugins": { - "basic-auth": { - "username": "foo", - "password": "bar", - "hide_auth_header": false - } - } - } - }, - "action": "set" - }]] - ) - - ngx.status = code - ngx.say(body) - } - } ---- request -GET /t ---- error_code: 200 ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 16: enable basic auth plugin using admin api ---- config - location /t { - content_by_lua_block { - local t = require("lib.test_admin").test - local code, body = t('/apisix/admin/routes/1', - ngx.HTTP_PUT, - [[{ - "plugins": { - "basic-auth": {} - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uri": "/hello" - }]] - ) - - if code >= 300 then - ngx.status = code - end - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 17: verify with hide auth header ---- request -GET /hello ---- more_headers -Authorization: Basic Zm9vOmJhcg== ---- response_body -hello world ---- response_headers_like -Authentication: ---- no_error_log -[error] ---- error_log -find consumer foo

Y)*ARw!H@kNaL=5(;D7Sr!vF9E3K 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