From 5b95b85faea3094d5e466ee2d39a52f1f805abbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:24:48 +0800 Subject: [PATCH 01/63] build(deps): bump actions/setup-node from 3.2.0 to 3.3.0 (#7203) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v3.2.0...v3.3.0) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] 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 cd71d8bdffff..d6b64921b0da 100644 --- a/.github/workflows/doc-lint.yml +++ b/.github/workflows/doc-lint.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: 🚀 Use Node.js - uses: actions/setup-node@v3.2.0 + uses: actions/setup-node@v3.3.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 431801b6a849..2338100168a7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,7 +32,7 @@ jobs: uses: actions/checkout@v3 - name: Setup Nodejs env - uses: actions/setup-node@v3.2.0 + uses: actions/setup-node@v3.3.0 with: node-version: '12' From a99eb64f23737a8796606f48b851e27be52a3e6e Mon Sep 17 00:00:00 2001 From: feihan <97138894+hf400159@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:44:17 +0800 Subject: [PATCH 02/63] docs: update skywalking Chinese doc (#7170) --- docs/en/latest/plugins/skywalking.md | 3 +- docs/zh/latest/plugins/skywalking.md | 259 +++++++++++++-------------- 2 files changed, 126 insertions(+), 136 deletions(-) diff --git a/docs/en/latest/plugins/skywalking.md b/docs/en/latest/plugins/skywalking.md index 016f7cc29331..44976d69b841 100644 --- a/docs/en/latest/plugins/skywalking.md +++ b/docs/en/latest/plugins/skywalking.md @@ -4,8 +4,7 @@ keywords: - APISIX - Plugin - SkyWalking - - skywalking -description: This document contains information about the Apache skywalking Plugin. +description: This document will introduce how the API gateway Apache APISIX reports metrics to Apache SkyWalking (an open-source APM) with the skywalking plugin. --- ## 测试插件 -### 运行 SkyWalking 实例 - -#### 例子: - -1. 启动 SkyWalking OAP 服务: - - SkyWalking 默认使用 H2 存储,可直接启动 - - ```shell - sudo docker run --name skywalking -d -p 1234:1234 -p 11800:11800 -p 12800:12800 --restart always apache/skywalking-oap-server:8.7.0-es6 - ``` +首先你可以通过 [Docker Compose](https://docs.docker.com/compose/install/) 启动 SkyWalking OAP 和 SkyWalking UI: - - 也许你会更倾向于使用 Elasticsearch 存储 - 1. 则需要先安装 Elasticsearch: + - 在 usr/local 中创建 `skywalking.yaml` 文件。 - ```shell - sudo docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 --restart always -e "discovery.type=single-node" elasticsearch:6.7.2 - ``` + ```yaml + version: "3" + services: + oap: + image: apache/skywalking-oap-server:8.9.1 + restart: always + ports: + - "12800:12800/tcp" - 2.【可选】安装 ElasticSearch 管理界面 elasticsearch-hq + ui: + image: apache/skywalking-ui:8.9.1 + restart: always + ports: + - "8080:8080/tcp" + environment: + SW_OAP_ADDRESS: http://oap:12800 + ``` - ```shell - sudo docker run -d --name elastic-hq -p 5000:5000 --restart always elastichq/elasticsearch-hq - ``` + - 使用以下命令启动上述创建的文件: - 3. 启动 SkyWalking OAP 服务: + ```shell + docker-compose -f skywalking.yaml up -d + ``` - ```shell - sudo docker run --name skywalking -d -p 1234:1234 -p 11800:11800 -p 12800:12800 --restart always --link elasticsearch:elasticsearch -e SW_STORAGE=elasticsearch -e SW_STORAGE_ES_CLUSTER_NODES=elasticsearch:9200 apache/skywalking-oap-server:8.7.0-es6 - ``` + 完成上述操作后,就已经启动了 SkyWalking 以及 SkyWalking Web UI。你可以使用以下命令确认容器是否正常运行: -2. SkyWalking Web UI: - 1. 启动管理系统: + ```shell + docker ps + ``` - ```shell - sudo docker run --name skywalking-ui -d -p 8080:8080 --link skywalking:skywalking -e SW_OAP_ADDRESS=skywalking:12800 --restart always apache/skywalking-ui - ``` +接下来你可以通过以下命令访问 APISIX: - 2. 打开 Web UI 页面 - 在浏览器里面输入 http://10.110.149.175:8080 如出现如下界面,则表示安装成功 - - ![plugin_skywalking](../../../assets/images/plugin/skywalking-3.png) +```shell +curl -v http://10.110.149.192:9080/uid/12 +``` -3. 测试示例: - - 通过访问 APISIX,访问上游服务 +``` +HTTP/1.1 200 OK +OK +... +``` - ```bash - $ curl -v http://10.110.149.192:9080/uid/12 - HTTP/1.1 200 OK - OK - ... - ``` +完成上述步骤后,打开浏览器,访问 SkyWalking 的 UI 页面,你可以看到如下服务拓扑图: - - 打开浏览器,访问 SkyWalking 的 UI 页面: +![plugin_skywalking](../../../assets/images/plugin/skywalking-4.png) - ```bash - http://10.110.149.175:8080/ - ``` +并且可以看到服务追踪列表: - 可以看到服务拓扑图\ - ![plugin_skywalking](../../../assets/images/plugin/skywalking-4.png)\ - 可以看到服务追踪列表\ - ![plugin_skywalking](../../../assets/images/plugin/skywalking-5.png) +![plugin_skywalking](../../../assets/images/plugin/skywalking-5.png) ## 禁用插件 -当你想禁用一条路由/服务上的 SkyWalking 插件的时候,很简单,在插件的配置中把对应的 JSON 配置删除即可,无须重启服务,即刻生效: +当你需要禁用 `skywalking` 插件时,可通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: ```shell -$ curl 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 ' { "methods": ["GET"], "uris": [ @@ -181,58 +220,10 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 }' ``` -现在就已经移除了 SkyWalking 插件了。其他插件的开启和移除也是同样的方法。 - -如果你想完全禁用 SkyWalking 插件,比如停掉后台上报数据的定时器,需要在 `config.yaml` 里把插件注释掉: +如果你想完全禁用 `skywalking` 插件,即停掉后台上报数据的定时器,就需要从配置文件(`./conf/config.yaml`)注释该插件: -```yaml +```yaml title="./conf/config.yaml" plugins: - - ... # plugin you need + - ... #- skywalking ``` - -然后重载 APISIX 即可。 - -## 上游服务为 SpringBoot 的示例代码 - -```java -package com.lenovo.ai.controller; - -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; - -/** - * @author cyxinda - * @create 2020-05-29 14:02 - * @desc skywalking test controller - **/ -@RestController -public class TestController { - @RequestMapping("/uid/{count}") - public String getUidList(@PathVariable("count") String countStr, HttpServletRequest request) { - System.out.println("counter:::::"+countStr); - return "OK"; - } -} - -``` - -启动服务的时候,需要配置 SkyWalking agent。 - -通过 `agent/config/agent.config` 修改配置 - -```shell -agent.service_name=yourservername -collector.backend_service=10.110.149.175:11800 -``` - -启动服务脚本: - -```shell -nohup java -javaagent:/root/skywalking/app/agent/skywalking-agent.jar \ --jar /root/skywalking/app/app.jar \ ---server.port=8089 \ -2>&1 > /root/skywalking/app/logs/nohup.log & -``` From 3d08d6bb4e9baab40e31f72497d44f9da20db2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 9 Jun 2022 10:14:24 +0800 Subject: [PATCH 03/63] chore: require http_stub_status_module exists (#7208) The http_stub_status_module is required in fact, as: 1. missing this module will generate "failed to fetch Nginx status" error: https://github.com/apache/apisix/blob/a99eb64f23737a8796606f48b851e27be52a3e6e/apisix/plugins/prometheus/exporter.lua#L245 2. both OpenResty and APISIX-Base already contains this module Therefore I decide to remove the additional check to make code simpler. Signed-off-by: spacewander --- apisix/cli/ngx_tpl.lua | 6 ------ apisix/cli/ops.lua | 8 ++------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 4709362e5913..cf8c08b38bb7 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -82,13 +82,11 @@ http { } } - {% if with_module_status then %} location = /apisix/nginx_status { allow 127.0.0.0/24; deny all; stub_status; } - {% end %} } } {% end %} @@ -503,13 +501,11 @@ http { } } - {% if with_module_status then %} location = /apisix/nginx_status { allow 127.0.0.0/24; deny all; stub_status; } - {% end %} } {% end %} @@ -618,14 +614,12 @@ http { {% end %} # http server configuration snippet ends - {% if with_module_status then %} location = /apisix/nginx_status { allow 127.0.0.0/24; deny all; access_log off; stub_status; } - {% end %} {% if enable_admin and not admin_server_addr then %} location /apisix/admin { diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 937d741060cb..0be0701f6827 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -244,12 +244,9 @@ Please modify "admin_key" in conf/config.yaml . end local or_info = util.execute_cmd("openresty -V 2>&1") - local with_module_status = true if or_info and not or_info:find("http_stub_status_module", 1, true) then - stderr:write("'http_stub_status_module' module is missing in ", - "your openresty, please check it out. Without this ", - "module, there will be fewer monitoring indicators.\n") - with_module_status = false + util.die("'http_stub_status_module' module is missing in ", + "your openresty, please check it out.\n") end local use_apisix_openresty = true @@ -548,7 +545,6 @@ Please modify "admin_key" in conf/config.yaml . lua_cpath = env.pkg_cpath_org, os_name = util.trim(util.execute_cmd("uname")), apisix_lua_home = env.apisix_home, - with_module_status = with_module_status, use_apisix_openresty = use_apisix_openresty, error_log = {level = "warn"}, enable_http = enable_http, From ce69c86b2a73ecd63409a272e4899a969248b8f6 Mon Sep 17 00:00:00 2001 From: mango <35127166+mangoGoForward@users.noreply.github.com> Date: Thu, 9 Jun 2022 10:17:03 +0800 Subject: [PATCH 04/63] docs(proxy-rewrite): remove empty space (#7210) Signed-off-by: mango --- docs/zh/latest/plugins/proxy-rewrite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/latest/plugins/proxy-rewrite.md b/docs/zh/latest/plugins/proxy-rewrite.md index 52c5e07cca22..4ef8e81eb725 100644 --- a/docs/zh/latest/plugins/proxy-rewrite.md +++ b/docs/zh/latest/plugins/proxy-rewrite.md @@ -34,7 +34,7 @@ description: 本文介绍了关于 Apache APISIX `proxy-rewrite` 插件的基本 ## 属性 | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | -| --------- | ------------- | ----- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------| +| --------- | ------------- | ----- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | scheme | string | 否 | "http" | ["http", "https"] | 不推荐使用。应该在 Upstream 的 `scheme` 字段设置上游的 `scheme`。| | uri | string | 否 | | | 转发到上游的新 `uri` 地址。支持 [NGINX variables](https://nginx.org/en/docs/http/ngx_http_core_module.html) 变量,例如:`$arg_name`。 | | method | string | 否 | | ["GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS","MKCOL", "COPY", "MOVE", "PROPFIND", "PROPFIND","LOCK", "UNLOCK", "PATCH", "TRACE"] | 将路由的请求方法代理为该请求方法。 | From 1b0c182ea007acccaabda3c13d7f4102da3944d9 Mon Sep 17 00:00:00 2001 From: Zeping Bai Date: Thu, 9 Jun 2022 14:09:46 +0800 Subject: [PATCH 05/63] fix(response-rewrite): schema format error (#7212) --- apisix/plugins/response-rewrite.lua | 10 +- t/plugin/response-rewrite2.t | 198 ++++++++++++---------------- 2 files changed, 93 insertions(+), 115 deletions(-) diff --git a/apisix/plugins/response-rewrite.lua b/apisix/plugins/response-rewrite.lua index b2c94f2aff1e..9a4015fb98bb 100644 --- a/apisix/plugins/response-rewrite.lua +++ b/apisix/plugins/response-rewrite.lua @@ -86,9 +86,15 @@ local schema = { }, }, }, - oneOf = {"body", "filters"}, }, - minProperties = 1, + dependencies = { + body = { + ["not"] = {required = {"filters"}} + }, + filters = { + ["not"] = {required = {"body"}} + } + } } diff --git a/t/plugin/response-rewrite2.t b/t/plugin/response-rewrite2.t index 88712888a28b..48401f915308 100644 --- a/t/plugin/response-rewrite2.t +++ b/t/plugin/response-rewrite2.t @@ -30,11 +30,79 @@ repeat_each(1); no_long_string(); no_shuffle(); 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: add plugin with valid filters +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local test_cases = { + {body = "test"}, + {filters = { + { + regex = "l", + replace = "m", + }, + }}, + {body = "test", filters = { + { + regex = "l", + replace = "m", + }, + }}, + {filters = {}}, + {filters = { + {regex = "l"}, + }}, + {filters = { + { + regex = "", + replace = "m", + }, + }}, + {filters = { + { + regex = "l", + replace = "m", + scope = "" + }, + }}, + } + local plugin = require("apisix.plugins.response-rewrite") + + for _, case in ipairs(test_cases) do + local ok, err = plugin.check_schema(case) + ngx.say(ok and "done" or err) + end + } + } +--- response_body eval +qr/done +done +failed to validate dependent schema for "filters|body": value wasn't supposed to match schema +property "filters" validation failed: expect array to have at least 1 items +property "filters" validation failed: failed to validate item 1: property "replace" is required +property "filters" validation failed: failed to validate item 1: property "regex" validation failed: string too short, expected at least 1, got 0 +property "filters" validation failed: failed to validate item 1: property "scope" validation failed: matches none of the enum values/ + + + +=== TEST 2: add plugin with valid filters --- config location /t { content_by_lua_block { @@ -56,16 +124,12 @@ __DATA__ ngx.say("done") } } ---- request -GET /t --- response_body done ---- no_error_log -[error] -=== TEST 2: add plugin with invalid filter required filed +=== TEST 3: add plugin with invalid filter required filed --- config location /t { content_by_lua_block { @@ -84,16 +148,12 @@ done end } } ---- request -GET /t --- response_body property "filters" validation failed: failed to validate item 1: property "replace" is required ---- no_error_log -[error] -=== TEST 3: add plugin with invalid filter scope +=== TEST 4: add plugin with invalid filter scope --- config location /t { content_by_lua_block { @@ -115,16 +175,12 @@ property "filters" validation failed: failed to validate item 1: property "repla end } } ---- request -GET /t --- response_body property "filters" validation failed: failed to validate item 1: property "scope" validation failed: matches none of the enum values ---- no_error_log -[error] -=== TEST 4: add plugin with invalid filter empty value +=== TEST 5: add plugin with invalid filter empty value --- config location /t { content_by_lua_block { @@ -144,16 +200,12 @@ property "filters" validation failed: failed to validate item 1: property "scope end } } ---- request -GET /t --- response_body property "filters" validation failed: failed to validate item 1: property "regex" validation failed: string too short, expected at least 1, got 0 ---- no_error_log -[error] -=== TEST 5: add plugin with invalid filter regex options +=== TEST 6: add plugin with invalid filter regex options --- config location /t { content_by_lua_block { @@ -174,18 +226,14 @@ property "filters" validation failed: failed to validate item 1: property "regex end } } ---- request -GET /t --- error_code eval 200 --- response_body regex "hello" validation failed: unknown flag "h" (flags "h") ---- no_error_log -[error] -=== TEST 6: set route with filters and vars expr +=== TEST 7: set route with filters and vars expr --- config location /t { content_by_lua_block { @@ -219,16 +267,12 @@ regex "hello" validation failed: unknown flag "h" (flags "h") ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] -=== TEST 7: check http body that matches filters +=== TEST 8: check http body that matches filters --- request GET /hello --- response_body @@ -236,7 +280,7 @@ test world -=== TEST 8: filter substitute global +=== TEST 9: filter substitute global --- config location /t { content_by_lua_block { @@ -271,16 +315,12 @@ test world ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] -=== TEST 9: check http body that substitute global +=== TEST 10: check http body that substitute global --- request GET /hello --- response_body @@ -288,7 +328,7 @@ hetto wortd -=== TEST 10: filter replace with empty +=== TEST 11: filter replace with empty --- config location /t { content_by_lua_block { @@ -322,16 +362,12 @@ hetto wortd ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] -=== TEST 11: check http body that replace with empty +=== TEST 12: check http body that replace with empty --- request GET /hello --- response_body @@ -339,7 +375,7 @@ GET /hello -=== TEST 12: filter replace with words +=== TEST 13: filter replace with words --- config location /t { content_by_lua_block { @@ -373,16 +409,12 @@ GET /hello ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] -=== TEST 13: check http body that replace with words +=== TEST 14: check http body that replace with words --- request GET /hello --- response_body @@ -390,59 +422,7 @@ hello * -=== TEST 14: set body and filters(body no effect) ---- 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": { - "response-rewrite": { - "vars": [ - ["status","==",200] - ], - "body": "new body", - "filters": [ - { - "regex": "hello", - "replace": "HELLO" - } - ] - } - }, - "upstream": { - "nodes": { - "127.0.0.1:1980": 1 - }, - "type": "roundrobin" - }, - "uris": ["/hello"] - }]] - ) - - ngx.say(body) - } - } ---- request -GET /t ---- response_body -passed ---- no_error_log -[error] - - - -=== TEST 15: check http body that set body and filters ---- request -GET /hello ---- response_body -HELLO world - - - -=== TEST 16: set multiple filters +=== TEST 15: set multiple filters --- config location /t { content_by_lua_block { @@ -480,16 +460,12 @@ HELLO world ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] -=== TEST 17: check http body that set multiple filters +=== TEST 16: check http body that set multiple filters --- request GET /hello --- response_body @@ -497,7 +473,7 @@ HETLO world -=== TEST 18: filters no any match +=== TEST 17: filters no any match --- config location /t { content_by_lua_block { @@ -531,16 +507,12 @@ HETLO world ngx.say(body) } } ---- request -GET /t --- response_body passed ---- no_error_log -[error] -=== TEST 19: check http body that filters no any match +=== TEST 18: check http body that filters no any match --- request GET /hello --- response_body From 975038ea70dc47e0613c42b3eabb8dcc730216a4 Mon Sep 17 00:00:00 2001 From: Ming Wen Date: Thu, 9 Jun 2022 17:33:54 +0800 Subject: [PATCH 06/63] docs: add API Gateway keyword and AWS graviton3. (#7217) --- README.md | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e3522db2b2eb..3d1ea19c4803 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ # --> -# Apache APISIX +# Apache APISIX API Gateway APISIX logo @@ -27,11 +27,11 @@ [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/apache/apisix.svg)](http://isitmaintained.com/project/apache/apisix "Average time to resolve an issue") [![Percentage of issues still open](http://isitmaintained.com/badge/open/apache/apisix.svg)](http://isitmaintained.com/project/apache/apisix "Percentage of issues still open") -**Apache APISIX** is a dynamic, real-time, high-performance API gateway. +**Apache APISIX** is a dynamic, real-time, high-performance API Gateway. -APISIX provides rich traffic management features such as load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability, and more. +APISIX API Gateway provides rich traffic management features such as load balancing, dynamic upstream, canary release, circuit breaking, authentication, observability, and more. -You can use Apache APISIX to handle traditional north-south traffic, +You can use **APISIX API Gateway** to handle traditional north-south traffic, as well as east-west traffic between services. It can also be used as a [k8s ingress controller](https://github.com/apache/apisix-ingress-controller). @@ -45,25 +45,18 @@ The technical architecture of Apache APISIX: - QQ Group - 552030619, 781365357 - Slack Workspace - [invitation link](https://join.slack.com/t/the-asf/shared_invite/zt-vlfbf7ch-HkbNHiU_uDlcH_RvaHv9gQ) (Please open an [issue](https://apisix.apache.org/docs/general/submit-issue) if this link is expired), and then join the #apisix channel (Channels -> Browse channels -> search for "apisix"). - ![Twitter Follow](https://img.shields.io/twitter/follow/ApacheAPISIX?style=social) - follow and interact with us using hashtag `#ApacheAPISIX` -- **Good first issues**: - - [Apache APISIX®](https://github.com/apache/apisix/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - - [Apache APISIX® Ingress Controller](https://github.com/apache/apisix-ingress-controller/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - - [Apache APISIX® dashboard](https://github.com/apache/apisix-dashboard/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - - [Apache APISIX® Helm Chart](https://github.com/apache/apisix-helm-chart/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - - [Docker distribution for Apache APISIX®](https://github.com/apache/apisix-docker/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - - [Apache APISIX® Website](https://github.com/apache/apisix-website/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - - [Apache APISIX® Java Plugin Runner](https://github.com/apache/apisix-java-plugin-runner/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) - - [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) +- [Documentation](https://apisix.apache.org/docs/) +- [Discussions](https://github.com/apache/apisix/discussions) +- [Blog](https://apisix.apache.org/blog) ## Features -You can use Apache APISIX as a traffic entrance to process all business data, including dynamic routing, dynamic upstream, dynamic certificates, +You can use APISIX API Gateway as a traffic entrance to process all business data, including dynamic routing, dynamic upstream, dynamic certificates, A/B testing, canary release, blue-green deployment, limit rate, defense against malicious attacks, metrics, monitoring alarms, service observability, service governance, etc. - **All platforms** - - Cloud-Native: Platform agnostic, No vendor lock-in, APISIX can run from bare-metal to Kubernetes. + - Cloud-Native: Platform agnostic, No vendor lock-in, APISIX API Gateway can run from bare-metal to Kubernetes. - Supports ARM64: Don't worry about the lock-in of the infra technology. - **Multi protocols** @@ -194,6 +187,8 @@ Using AWS's eight-core server, APISIX's QPS reaches 140,000 with a latency of on [Benchmark script](benchmark/run.sh) has been open sourced, welcome to try and contribute. +[The APISIX APISIX Gateway also works perfectly in AWS graviton3 C7g.](https://apisix.apache.org/blog/2022/06/07/installation-performance-test-of-apigateway-apisix-on-aws-graviton3) + ## Contributor Over Time > [visit here](https://www.apiseven.com/contributor-graph) to generate Contributor Over Time. @@ -206,9 +201,9 @@ Using AWS's eight-core server, APISIX's QPS reaches 140,000 with a latency of on - [Copernicus Reference System Software](https://github.com/COPRS/infrastructure/wiki/Networking-trade-off) - [More Stories](https://apisix.apache.org/blog/tags/user-case) -## Who Uses APISIX? +## Who Uses APISIX API Gateway? -A wide variety of companies and organizations use APISIX for research, production and commercial product, below are some of them: +A wide variety of companies and organizations use APISIX API Gateway for research, production and commercial product, below are some of them: - Airwallex - Bilibili From e5f7cec543a4a9994e2a98763e9a813782725eb6 Mon Sep 17 00:00:00 2001 From: Zhendong Qi <88528414+zhendongcmss@users.noreply.github.com> Date: Fri, 10 Jun 2022 10:46:39 +0800 Subject: [PATCH 07/63] docs: add re case on response-rewrite plugin (#7197) --- docs/en/latest/plugins/response-rewrite.md | 65 ++++++++++++++++++++++ docs/zh/latest/plugins/response-rewrite.md | 65 ++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/docs/en/latest/plugins/response-rewrite.md b/docs/en/latest/plugins/response-rewrite.md index 80ce66e6ec68..a74bea830905 100644 --- a/docs/en/latest/plugins/response-rewrite.md +++ b/docs/en/latest/plugins/response-rewrite.md @@ -130,6 +130,71 @@ So, if you have configured the `response-rewrite` Plugin, it do a force overwrit ::: +The example below shows how you can replace a key in the response body. Here, the key X-Amzn-Trace-Id is replaced with X-Amzn-Trace-Id-Replace by configuring the filters attribute using regex: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins":{ + "response-rewrite":{ + "headers":{ + "X-Server-id":3, + "X-Server-status":"on", + "X-Server-balancer_addr":"$balancer_ip:$balancer_port" + }, + "filters":[ + { + "regex":"X-Amzn-Trace-Id", + "scope":"global", + "replace":"X-Amzn-Trace-Id-Replace" + } + ], + "vars":[ + [ + "status", + "==", + 200 + ] + ] + } + }, + "upstream":{ + "type":"roundrobin", + "scheme":"https", + "nodes":{ + "httpbin.org:443":1 + } + }, + "uri":"/*" +}' +``` + +```shell +curl -X GET -i http://127.0.0.1:9080/get +``` + +```shell +HTTP/1.1 200 OK +Transfer-Encoding: chunked +X-Server-status: on +X-Server-balancer-addr: 34.206.80.189:443 +X-Server-id: 3 + +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "127.0.0.1", + "User-Agent": "curl/7.29.0", + "X-Amzn-Trace-Id-Replace": "Root=1-629e0b89-1e274fdd7c23ca6e64145aa2", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "127.0.0.1, 117.136.46.203", + "url": "https://127.0.0.1/get" +} + +``` + ## Disable Plugin To disable the `response-rewrite` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. diff --git a/docs/zh/latest/plugins/response-rewrite.md b/docs/zh/latest/plugins/response-rewrite.md index a3701c8afc90..8cf1b379a1eb 100644 --- a/docs/zh/latest/plugins/response-rewrite.md +++ b/docs/zh/latest/plugins/response-rewrite.md @@ -129,6 +129,71 @@ X-Server-balancer_addr: 127.0.0.1:80 ::: +使用 `filters` 正则匹配将返回 body 的 X-Amzn-Trace-Id 替换为 X-Amzn-Trace-Id-Replace。 + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "plugins":{ + "response-rewrite":{ + "headers":{ + "X-Server-id":3, + "X-Server-status":"on", + "X-Server-balancer_addr":"$balancer_ip:$balancer_port" + }, + "filters":[ + { + "regex":"X-Amzn-Trace-Id", + "scope":"global", + "replace":"X-Amzn-Trace-Id-Replace" + } + ], + "vars":[ + [ + "status", + "==", + 200 + ] + ] + } + }, + "upstream":{ + "type":"roundrobin", + "scheme":"https", + "nodes":{ + "httpbin.org:443":1 + } + }, + "uri":"/*" +}' +``` + +```shell +curl -X GET -i http://127.0.0.1:9080/get +``` + +```shell +HTTP/1.1 200 OK +Transfer-Encoding: chunked +X-Server-status: on +X-Server-balancer-addr: 34.206.80.189:443 +X-Server-id: 3 + +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "127.0.0.1", + "User-Agent": "curl/7.29.0", + "X-Amzn-Trace-Id-Replace": "Root=1-629e0b89-1e274fdd7c23ca6e64145aa2", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "127.0.0.1, 117.136.46.203", + "url": "https://127.0.0.1/get" +} + +``` + ## 禁用插件 当你需要禁用 `response-rewrite` 插件时,可以通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: From 7188acbcbba263bec61a0a65f7ec6b5e37363006 Mon Sep 17 00:00:00 2001 From: sasakiyori <871118894@qq.com> Date: Fri, 10 Jun 2022 10:47:21 +0800 Subject: [PATCH 08/63] change: remove upstream.enable_websocket which is deprecated since 2020 (#7222) --- apisix/init.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/apisix/init.lua b/apisix/init.lua index d68e31ba5666..6dc072bdc579 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -486,9 +486,6 @@ function _M.http_access_phase() end local route_val = route.value - if route_val.upstream and route_val.upstream.enable_websocket then - enable_websocket = true - end api_ctx.matched_upstream = (route.dns_value and route.dns_value.upstream) From f0f5b48a8fbe78051e7ab97b979cc1632b2c9d2a Mon Sep 17 00:00:00 2001 From: Zhendong Qi <88528414+zhendongcmss@users.noreply.github.com> Date: Fri, 10 Jun 2022 12:48:44 +0800 Subject: [PATCH 09/63] fix: add debug yaml validation (#7201) --- apisix/debug.lua | 61 +++++++++++++++++++++++++++++++++++++++++- t/debug/dynamic-hook.t | 6 +++++ t/debug/hook.t | 34 +++++++++++++++++++++-- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/apisix/debug.lua b/apisix/debug.lua index 363aee172e4f..72c101635881 100644 --- a/apisix/debug.lua +++ b/apisix/debug.lua @@ -20,6 +20,7 @@ local log = require("apisix.core.log") local profile = require("apisix.core.profile") local lfs = require("lfs") local inspect = require("inspect") +local jsonschema = require("jsonschema") local io = io local ngx = ngx local re_find = ngx.re.find @@ -38,6 +39,51 @@ local debug_yaml_ctime local _M = {version = 0.1} +local config_schema = { + type = "object", + properties = { + basic = { + properties = { + enable = { + type = "boolean", + }, + } + }, + http_filter = { + properties = { + enable = { + type = "boolean", + }, + enable_header_name = { + type = "string", + }, + } + }, + hook_conf = { + properties = { + enable = { + type = "boolean", + }, + name = { + type = "string", + }, + log_level = { + enum = {"debug", "info", "notice", "warn", "error", + "crit", "alert","emerg"}, + }, + is_print_input_args = { + type = "boolean", + }, + is_print_return_value = { + type = "boolean", + }, + } + }, + }, + required = {"basic", "http_filter", "hook_conf"}, +} + + local function read_debug_yaml() local attributes, err = lfs.attributes(debug_yaml_path) if not attributes then @@ -93,6 +139,16 @@ local function read_debug_yaml() debug_yaml_new.hooks = debug_yaml_new.hooks or {} debug_yaml = debug_yaml_new debug_yaml_ctime = last_change_time + + -- validate the debug yaml config + local validator = jsonschema.generate_validator(config_schema) + local ok, err = validator(debug_yaml) + if not ok then + log.error("failed to validate debug config " .. err) + return + end + + return true end @@ -204,7 +260,10 @@ local function sync_debug_status(premature) return end - read_debug_yaml() + if not read_debug_yaml() then + return + end + sync_debug_hooks() end diff --git a/t/debug/dynamic-hook.t b/t/debug/dynamic-hook.t index 95ac63bcc83e..692942d1f9e4 100644 --- a/t/debug/dynamic-hook.t +++ b/t/debug/dynamic-hook.t @@ -213,6 +213,8 @@ call require("apisix").http_log_phase() return:{} === TEST 4: plugin filter log --- debug_config +basic: + enable: true http_filter: enable: true # enable or disable this feature enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable @@ -295,6 +297,8 @@ filter(): call require("apisix.plugin").filter() return:{ === TEST 5: multiple requests, only output logs of the request with enable_header_name --- debug_config +basic: + enable: true http_filter: enable: true enable_header_name: X-APISIX-Dynamic-Debug @@ -374,6 +378,8 @@ qr/call\srequire\(\"apisix.plugin\"\).filter\(\)\sreturn.*GET\s\/mysleep\?second === TEST 6: hook function with ctx as param --- debug_config +basic: + enable: true http_filter: enable: true # enable or disable this feature enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable diff --git a/t/debug/hook.t b/t/debug/hook.t index 5afac49ce589..1a9ebc140437 100644 --- a/t/debug/hook.t +++ b/t/debug/hook.t @@ -104,6 +104,11 @@ call require("apisix").http_log_phase() return:{} === TEST 4: plugin filter log --- debug_config +basic: + enable: true +http_filter: + enable: true # enable or disable this feature + enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable hook_conf: enable: true # enable or disable this feature name: hook_test # the name of module and function list @@ -120,10 +125,35 @@ hook_test: # module and function list, name: hook_test GET /hello --- more_headers Host: foo.com +X-APISIX-Dynamic-Debug: true --- response_body hello world ---- no_error_log -[error] --- error_log filter(): call require("apisix.plugin").filter() args:{ filter(): call require("apisix.plugin").filter() return:{ + + + +=== TEST 5: missing hook_conf +--- debug_config +basic: + enable: true +http_filter: + enable: true # enable or disable this feature + enable_header_name: X-APISIX-Dynamic-Debug # the header name of dynamic enable + +hook_test: # module and function list, name: hook_test + apisix.plugin: # required module name + - filter # function name + +#END +--- request +GET /hello +--- more_headers +Host: foo.com +X-APISIX-Dynamic-Debug: true +--- response_body +hello world +--- error_log +read_debug_yaml(): failed to validate debug config property "hook_conf" is required +--- wait: 3 From 5bdab07676e4c2130c5578949d1303be6c11ece0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Fri, 10 Jun 2022 15:51:24 +0800 Subject: [PATCH 10/63] test: remove unused required etcd (#7225) They are introduced by Copy & Paste Signed-off-by: spacewander --- t/admin/plugin-configs.t | 2 -- t/admin/proto.t | 4 ---- t/admin/stream-routes.t | 1 - t/cli/test_main.sh | 1 + t/plugin/grpc-transcode2.t | 2 -- t/xrpc/pingpong2.t | 1 - t/xrpc/redis.t | 4 ---- t/xrpc/redis2.t | 1 - 8 files changed, 1 insertion(+), 15 deletions(-) diff --git a/t/admin/plugin-configs.t b/t/admin/plugin-configs.t index a9822037683f..1f0da8a2a463 100644 --- a/t/admin/plugin-configs.t +++ b/t/admin/plugin-configs.t @@ -286,7 +286,6 @@ passed location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/plugin_configs/1', ngx.HTTP_PUT, [[{ @@ -413,7 +412,6 @@ passed location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/plugin_configs/1', ngx.HTTP_PUT, [[{ diff --git a/t/admin/proto.t b/t/admin/proto.t index bab49c933b6c..3a05a26df9a8 100644 --- a/t/admin/proto.t +++ b/t/admin/proto.t @@ -43,7 +43,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, message = t('/apisix/admin/proto/1', ngx.HTTP_PUT, [[{ @@ -89,7 +88,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, message = t('/apisix/admin/proto/1', ngx.HTTP_DELETE, nil, @@ -117,7 +115,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, message = t('/apisix/admin/proto/2', ngx.HTTP_PUT, [[{ @@ -210,7 +207,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, message = t('/apisix/admin/proto/1', ngx.HTTP_PUT, [[{ diff --git a/t/admin/stream-routes.t b/t/admin/stream-routes.t index 6552165b5221..01062fbc84f8 100644 --- a/t/admin/stream-routes.t +++ b/t/admin/stream-routes.t @@ -605,7 +605,6 @@ xrpc: location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") for _, case in ipairs({ {input = { name = "xxx", diff --git a/t/cli/test_main.sh b/t/cli/test_main.sh index 73202a8f3ce0..ea54c53b8425 100755 --- a/t/cli/test_main.sh +++ b/t/cli/test_main.sh @@ -705,6 +705,7 @@ fi ./bin/apisix stop sleep 0.5 +rm logs/nginx.pid || true # check no corresponding process make run diff --git a/t/plugin/grpc-transcode2.t b/t/plugin/grpc-transcode2.t index e9c6c0396a7e..7c8286650f50 100644 --- a/t/plugin/grpc-transcode2.t +++ b/t/plugin/grpc-transcode2.t @@ -41,7 +41,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/proto/1', ngx.HTTP_PUT, [[{ @@ -136,7 +135,6 @@ Content-Type: application/json location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/proto/2', ngx.HTTP_PUT, [[{ diff --git a/t/xrpc/pingpong2.t b/t/xrpc/pingpong2.t index cdcf367c2f09..fc77fa1482df 100644 --- a/t/xrpc/pingpong2.t +++ b/t/xrpc/pingpong2.t @@ -110,7 +110,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_PUT, { diff --git a/t/xrpc/redis.t b/t/xrpc/redis.t index 2d7c276c3eba..afb2f40e67ee 100644 --- a/t/xrpc/redis.t +++ b/t/xrpc/redis.t @@ -60,7 +60,6 @@ __DATA__ location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_PUT, { @@ -265,7 +264,6 @@ hget animals: bark location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_PUT, { @@ -365,7 +363,6 @@ ok location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_PUT, { @@ -464,7 +461,6 @@ ok location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_PUT, { diff --git a/t/xrpc/redis2.t b/t/xrpc/redis2.t index aad5c2fb8b05..65ca9829c616 100644 --- a/t/xrpc/redis2.t +++ b/t/xrpc/redis2.t @@ -103,7 +103,6 @@ passed location /t { content_by_lua_block { local t = require("lib.test_admin").test - local etcd = require("apisix.core.etcd") local code, body = t('/apisix/admin/stream_routes/1', ngx.HTTP_PUT, { From b0bfd3987cfe8977369b445f4879a275086c2558 Mon Sep 17 00:00:00 2001 From: likzn <1020193211@qq.com> Date: Sun, 12 Jun 2022 19:29:03 +0800 Subject: [PATCH 11/63] docs: make company on README more preciser (#7230) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d1ea19c4803..e28bef917c13 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ A wide variety of companies and organizations use APISIX API Gateway for researc - Tencent Game - Travelsky - VIVO -- weibo +- Sina Weibo - WPS ## Landscape From 932f7b37bbb696ec7e6df58f7c9727fe2080c85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 13 Jun 2022 16:52:29 +0800 Subject: [PATCH 12/63] fix: distinguish different upstreams even they have the same addr (#7213) Thanks for the report of @redynasc Signed-off-by: spacewander --- apisix/upstream.lua | 7 +++-- t/node/upstream.t | 70 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/apisix/upstream.lua b/apisix/upstream.lua index a0e963b44f8a..d314de49d7f9 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -286,8 +286,11 @@ function _M.set_by_route(route, api_ctx) end end - set_directly(api_ctx, up_conf.type .. "#upstream_" .. tostring(up_conf), - tostring(up_conf), up_conf) + local id = up_conf.parent.value.id + local conf_version = up_conf.parent.modifiedIndex + -- include the upstream object as part of the version, because the upstream will be changed + -- by service discovery or dns resolver. + set_directly(api_ctx, id, conf_version .. "#" .. 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/upstream.t b/t/node/upstream.t index 704a0259da2d..70da36145b9b 100644 --- a/t/node/upstream.t +++ b/t/node/upstream.t @@ -624,3 +624,73 @@ passed GET /uri --- error_log Host: 127.0.0.1:1979 + + + +=== TEST 25: distinguish different upstreams even they have the same addr +--- 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, + { + nodes = {["localhost:1980"] = 1}, + type = "roundrobin" + } + ) + assert(code < 300) + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "upstream_id": "1", + "uri": "/server_port" + }]] + ) + assert(code < 300) + + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/server_port" + + local ports_count = {} + for i = 1, 24 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ports_count[res.body] = (ports_count[res.body] or 0) + 1 + + local code, body = t('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + { + nodes = {["localhost:" .. (1980 + i % 3)] = 1}, + type = "roundrobin" + } + ) + assert(code < 300) + end + + local ports_arr = {} + for port, count in pairs(ports_count) do + table.insert(ports_arr, {port = port, count = count}) + end + + local function cmd(a, b) + return a.port > b.port + end + table.sort(ports_arr, cmd) + + ngx.say(require("toolkit.json").encode(ports_arr)) + } + } +--- request +GET /t +--- timeout: 5 +--- response_body +[{"count":8,"port":"1982"},{"count":8,"port":"1981"},{"count":8,"port":"1980"}] +--- no_error_log +[error] From eb62ac05468cffdadf63664b499bb699a290827d Mon Sep 17 00:00:00 2001 From: soulbird Date: Mon, 13 Jun 2022 16:52:51 +0800 Subject: [PATCH 13/63] fix: duplicate X-Forwarded-Proto will be sent as string (#7229) Co-authored-by: soulbird --- apisix/init.lua | 7 ++++++- t/plugin/proxy-rewrite2.t | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apisix/init.lua b/apisix/init.lua index 6dc072bdc579..f26dee4e1a7f 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -56,6 +56,7 @@ local str_byte = string.byte local str_sub = string.sub local tonumber = tonumber local pairs = pairs +local type = type local control_api_router local is_http = false @@ -229,7 +230,11 @@ local function set_upstream_headers(api_ctx, picked_server) local hdr = core.request.header(api_ctx, "X-Forwarded-Proto") if hdr then - api_ctx.var.var_x_forwarded_proto = hdr + if type(hdr) == "table" then + api_ctx.var.var_x_forwarded_proto = hdr[1] + else + api_ctx.var.var_x_forwarded_proto = hdr + end end end diff --git a/t/plugin/proxy-rewrite2.t b/t/plugin/proxy-rewrite2.t index 4fbfe55bab34..fcd4011bacec 100644 --- a/t/plugin/proxy-rewrite2.t +++ b/t/plugin/proxy-rewrite2.t @@ -208,3 +208,27 @@ X-Forwarded-Proto: grpc X-Forwarded-Proto: https-rewrite --- error_log localhost + + + +=== TEST 7: pass duplicate X-Forwarded-Proto +--- apisix_yaml +routes: + - + id: 1 + uri: /echo + upstream_id: 1 +upstreams: + - + id: 1 + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +#END +--- request +GET /echo +--- more_headers +X-Forwarded-Proto: http +X-Forwarded-Proto: grpc +--- response_headers +X-Forwarded-Proto: http From 75107ab40e7f2a4e74d4881776f6143ac31a8228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 14 Jun 2022 09:27:46 +0800 Subject: [PATCH 14/63] fix(api-response): check response header format (#7238) --- apisix/plugins/api-breaker.lua | 3 ++- docs/en/latest/plugins/api-breaker.md | 2 +- docs/zh/latest/plugins/api-breaker.md | 2 +- t/plugin/api-breaker.t | 33 +++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apisix/plugins/api-breaker.lua b/apisix/plugins/api-breaker.lua index 5ccf4404082a..eabca140af11 100644 --- a/apisix/plugins/api-breaker.lua +++ b/apisix/plugins/api-breaker.lua @@ -53,7 +53,8 @@ local schema = { type = "string", minLength = 1 } - } + }, + required = {"key", "value"}, } }, max_breaker_sec = { diff --git a/docs/en/latest/plugins/api-breaker.md b/docs/en/latest/plugins/api-breaker.md index 87c1f1d58cf5..4469b5a31d40 100644 --- a/docs/en/latest/plugins/api-breaker.md +++ b/docs/en/latest/plugins/api-breaker.md @@ -43,7 +43,7 @@ In an unhealthy state, if the Upstream service responds with a status code from |-------------------------|----------------|----------|---------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | break_response_code | integer | True | | [200, ..., 599] | HTTP error code to return when Upstream is unhealthy. | | break_response_body | string | False | | | Body of the response message to return when Upstream is unhealthy. | -| break_response_headers | array[object] | False | | | Headers of the response message to return when Upstream is unhealthy. Can only be configured when the `break_response_body` attribute is configured. The values can contain Nginx variables. For example, `$remote_addr` and `$balancer_ip`. | +| break_response_headers | array[object] | False | | [{"key":"header_name","value":"can contain Nginx $var"}] | Headers of the response message to return when Upstream is unhealthy. Can only be configured when the `break_response_body` attribute is configured. The values can contain APISIX variables. For example, we can use `{"key":"X-Client-Addr","value":"$remote_addr:$remote_port"}`. | | max_breaker_sec | integer | False | 300 | >=3 | Maximum time in seconds for circuit breaking. | | unhealthy.http_statuses | array[integer] | False | [500] | [500, ..., 599] | Status codes of Upstream to be considered unhealthy. | | unhealthy.failures | integer | False | 3 | >=1 | Number of consecutive failures for the Upstream service to be considered unhealthy. | diff --git a/docs/zh/latest/plugins/api-breaker.md b/docs/zh/latest/plugins/api-breaker.md index 6672f8d4fa44..0e00517b259a 100644 --- a/docs/zh/latest/plugins/api-breaker.md +++ b/docs/zh/latest/plugins/api-breaker.md @@ -45,7 +45,7 @@ title: api-breaker | ----------------------- | -------------- | ------ | ---------- | --------------- | -------------------------------- | | break_response_code | integer | 必须 | 无 | [200, ..., 599] | 不健康返回错误码 | | break_response_body | string | 可选 | 无 | | 不健康返回报文 | -| break_response_headers | array[object] | 可选 | 无 | | 不健康返回报文头,这里可以设置多个。这个值能够以 `$var` 的格式包含 Nginx 变量,比如 `$remote_addr $balancer_ip`。该字段仅在 `break_response_body` 被配置时生效 | +| break_response_headers | array[object] | 可选 | 无 | [{"key":"header_name","value":"can contain Nginx $var"}] | 不健康返回报文头,这里可以设置多个。该字段仅在 `break_response_body` 被配置时生效。这个值能够以 `$var` 的格式包含 APISIX 变量,比如 `{"key":"X-Client-Addr","value":"$remote_addr:$remote_port"}`。 | | max_breaker_sec | integer | 可选 | 300 | >=3 | 最大熔断持续时间 | | unhealthy.http_statuses | array[integer] | 可选 | {500} | [500, ..., 599] | 不健康时候的状态码 | | unhealthy.failures | integer | 可选 | 3 | >=1 | 触发不健康状态的连续错误请求次数 | diff --git a/t/plugin/api-breaker.t b/t/plugin/api-breaker.t index e1eccfb2b6b1..c63d87dba072 100644 --- a/t/plugin/api-breaker.t +++ b/t/plugin/api-breaker.t @@ -655,3 +655,36 @@ phase_func(): breaker_time: 10 --- response_body {"500":4,"502":16} --- timeout: 25 + + + +=== TEST 20: reject invalid schema +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + for _, case in ipairs({ + {input = { + break_response_code = 200, + break_response_headers = {{["content-type"] = "application/json"}} + }}, + }) do + local code, body = t('/apisix/admin/global_rules/1', + ngx.HTTP_PUT, + { + id = "1", + plugins = { + ["api-breaker"] = case.input + } + } + ) + ngx.print(require("toolkit.json").decode(body).error_msg) + end + } + } +--- request +GET /t +--- response_body eval +qr/failed to check the configuration of plugin api-breaker err: property \"break_response_headers\" validation failed: failed to validate item 1: property \"(key|value)\" is required/ +--- no_error_log +[error] From fc0dc15fcc430fe255cceab007e33ee03ce7025b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 14 Jun 2022 09:28:37 +0800 Subject: [PATCH 15/63] chore: validate etcd conf strictly (#7245) Signed-off-by: spacewander --- .github/workflows/chaos.yml | 3 +- apisix/cli/schema.lua | 14 ++++++- t/chaos/utils/Dockerfile | 75 +++++++++++++++++++++++++++++++++++ t/cli/test_validate_config.sh | 27 +++++++++++++ 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 t/chaos/utils/Dockerfile diff --git a/.github/workflows/chaos.yml b/.github/workflows/chaos.yml index 677b6150d6ee..20b45f602c90 100644 --- a/.github/workflows/chaos.yml +++ b/.github/workflows/chaos.yml @@ -40,9 +40,8 @@ jobs: - name: Creating minikube cluster run: | bash ./t/chaos/utils/setup_chaos_utils.sh start_minikube - wget https://raw.githubusercontent.com/apache/apisix-docker/master/alpine-local/Dockerfile mkdir logs - docker build -t apache/apisix:alpine-local --build-arg APISIX_PATH=. -f Dockerfile . + docker build -t apache/apisix:alpine-local --build-arg APISIX_PATH=. -f ./t/chaos/utils/Dockerfile . minikube cache add apache/apisix:alpine-local -v 7 --alsologtostderr - name: Print cluster information diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua index 8c7a873214c1..7afece3ab239 100644 --- a/apisix/cli/schema.lua +++ b/apisix/cli/schema.lua @@ -212,8 +212,20 @@ local config_schema = { type = "string", }, } + }, + prefix = { + type = "string", + pattern = [[^/[^/]+$]] + }, + host = { + type = "array", + items = { + type = "string", + pattern = [[^https?://]] + } } - } + }, + required = {"prefix", "host"} }, wasm = { type = "object", diff --git a/t/chaos/utils/Dockerfile b/t/chaos/utils/Dockerfile new file mode 100644 index 000000000000..700108283799 --- /dev/null +++ b/t/chaos/utils/Dockerfile @@ -0,0 +1,75 @@ +# +# 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. +# + +ARG ENABLE_PROXY=false + +FROM openresty/openresty:1.19.3.2-alpine-fat AS production-stage + +ARG ENABLE_PROXY +ARG APISIX_PATH +COPY $APISIX_PATH ./apisix +RUN set -x \ + && (test "${ENABLE_PROXY}" != "true" || /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories) \ + && apk add --no-cache --virtual .builddeps \ + automake \ + autoconf \ + libtool \ + pkgconfig \ + cmake \ + git \ + openldap-dev \ + pcre-dev \ + && cd apisix \ + && git config --global url.https://github.com/.insteadOf git://github.com/ \ + && make deps \ + && cp -v bin/apisix /usr/bin/ \ + && mv ../apisix /usr/local/apisix \ + && apk del .builddeps build-base make unzip + +FROM alpine:3.13 AS last-stage + +ARG ENABLE_PROXY +# add runtime for Apache APISIX +RUN set -x \ + && (test "${ENABLE_PROXY}" != "true" || /bin/sed -i 's,http://dl-cdn.alpinelinux.org,https://mirrors.aliyun.com,g' /etc/apk/repositories) \ + && apk add --no-cache \ + bash \ + curl \ + libstdc++ \ + openldap \ + pcre \ + tzdata + +WORKDIR /usr/local/apisix + +COPY --from=production-stage /usr/local/openresty/ /usr/local/openresty/ +COPY --from=production-stage /usr/local/apisix/ /usr/local/apisix/ +COPY --from=production-stage /usr/bin/apisix /usr/bin/apisix + +# forward request and error logs to docker log collector +RUN mkdir -p logs && touch logs/access.log && touch logs/error.log \ + && ln -sf /dev/stdout /usr/local/apisix/logs/access.log \ + && ln -sf /dev/stderr /usr/local/apisix/logs/error.log + +ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin + +EXPOSE 9080 9443 + +CMD ["sh", "-c", "/usr/bin/apisix init && /usr/bin/apisix init_etcd && /usr/local/openresty/bin/openresty -p /usr/local/apisix -g 'daemon off;'"] + +STOPSIGNAL SIGQUIT + diff --git a/t/cli/test_validate_config.sh b/t/cli/test_validate_config.sh index 164d530fe0a4..216f1d9fb14d 100755 --- a/t/cli/test_validate_config.sh +++ b/t/cli/test_validate_config.sh @@ -202,3 +202,30 @@ if echo "$out" | grep "missing loopback or unspecified in the nginx_config.http. fi echo "passed: check the realip configuration for batch-requests" + +echo ' +etcd: + host: + - 127.0.0.1 +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'property "host" validation failed'; then + echo "failed: should check etcd schema during init" + exit 1 +fi + +echo ' +etcd: + prefix: "/apisix/" + host: + - https://127.0.0.1 +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'property "prefix" validation failed'; then + echo "failed: should check etcd schema during init" + exit 1 +fi + +echo "passed: check etcd schema during init" From 45816272a80d6d2045259d32acdabbec4039ed1e Mon Sep 17 00:00:00 2001 From: soulbird Date: Tue, 14 Jun 2022 09:29:01 +0800 Subject: [PATCH 16/63] feat(ssl): support get upstream cert from ssl object (#7221) Co-authored-by: soulbird --- apisix/admin/ssl.lua | 2 +- apisix/init.lua | 23 +++++ apisix/schema_def.lua | 40 ++++++-- apisix/ssl.lua | 4 + apisix/ssl/router/radixtree_sni.lua | 18 +++- apisix/upstream.lua | 37 ++++++- docs/en/latest/admin-api.md | 8 +- docs/zh/latest/admin-api.md | 8 +- t/admin/ssl.t | 73 +++++++++++++- t/admin/ssl2.t | 8 +- t/admin/upstream.t | 133 +++++++++++++++++++++++++ t/node/upstream-mtls.t | 144 +++++++++++++++++++++++++++- 12 files changed, 477 insertions(+), 21 deletions(-) diff --git a/apisix/admin/ssl.lua b/apisix/admin/ssl.lua index 341e03004d1a..9a73107c9f10 100644 --- a/apisix/admin/ssl.lua +++ b/apisix/admin/ssl.lua @@ -46,7 +46,7 @@ local function check_conf(id, conf, need_id) conf.id = id core.log.info("schema: ", core.json.delay_encode(core.schema.ssl)) - core.log.info("conf : ", core.json.delay_encode(conf)) + core.log.info("conf: ", core.json.delay_encode(conf)) local ok, err = apisix_ssl.check_ssl_conf(false, conf) if not ok then diff --git a/apisix/init.lua b/apisix/init.lua index f26dee4e1a7f..af0f22553f1d 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -497,6 +497,29 @@ function _M.http_access_phase() or route_val.upstream end + if api_ctx.matched_upstream and api_ctx.matched_upstream.tls and + api_ctx.matched_upstream.tls.client_cert_id then + + local cert_id = api_ctx.matched_upstream.tls.client_cert_id + local upstream_ssl = router.router_ssl.get_by_id(cert_id) + if not upstream_ssl or upstream_ssl.type ~= "client" then + local err = upstream_ssl and + "ssl type should be 'client'" or + "ssl id [" .. cert_id .. "] not exits" + core.log.error("failed to get ssl cert: ", err) + + if is_http then + return core.response.exit(502) + end + + return ngx_exit(1) + end + + core.log.info("matched ssl: ", + core.json.delay_encode(upstream_ssl, true)) + api_ctx.upstream_ssl = upstream_ssl + end + if enable_websocket then api_ctx.var.upstream_upgrade = api_ctx.var.http_upgrade api_ctx.var.upstream_connection = api_ctx.var.http_connection diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 767c2fa63503..7d39b62aad76 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -404,6 +404,7 @@ local upstream_schema = { tls = { type = "object", properties = { + client_cert_id = id_schema, client_cert = certificate_scheme, client_key = private_key_schema, verify = { @@ -414,8 +415,17 @@ local upstream_schema = { }, }, dependencies = { - client_cert = {"client_key"}, - client_key = {"client_cert"}, + client_cert = { + required = {"client_key"}, + ["not"] = {required = {"client_cert_id"}} + }, + client_key = { + required = {"client_cert"}, + ["not"] = {required = {"client_cert_id"}} + }, + client_cert_id = { + ["not"] = {required = {"client_client", "client_key"}} + } } }, keepalive_pool = { @@ -504,7 +514,7 @@ local upstream_schema = { oneOf = { {required = {"type", "nodes"}}, {required = {"type", "service_name", "discovery_type"}}, - }, + } } -- TODO: add more nginx variable support @@ -722,6 +732,14 @@ _M.ssl = { type = "object", properties = { id = id_schema, + type = { + description = "ssl certificate type, " .. + "server to server certificate, " .. + "client to client certificate for upstream", + type = "string", + default = "server", + enum = {"server", "client"} + }, cert = certificate_scheme, key = private_key_schema, sni = { @@ -772,10 +790,20 @@ _M.ssl = { create_time = timestamp_def, update_time = timestamp_def }, - oneOf = { - {required = {"sni", "key", "cert"}}, - {required = {"snis", "key", "cert"}} + ["if"] = { + properties = { + type = { + enum = {"server"}, + }, + }, + }, + ["then"] = { + oneOf = { + {required = {"sni", "key", "cert"}}, + {required = {"snis", "key", "cert"}} + } }, + ["else"] = {required = {"key", "cert"}} } diff --git a/apisix/ssl.lua b/apisix/ssl.lua index c0c47aec07fd..7d48f308502e 100644 --- a/apisix/ssl.lua +++ b/apisix/ssl.lua @@ -197,6 +197,10 @@ function _M.check_ssl_conf(in_dp, conf) return nil, err end + if conf.type == "client" then + return true + end + local numcerts = conf.certs and #conf.certs or 0 local numkeys = conf.keys and #conf.keys or 0 if numcerts ~= numkeys then diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index 70ac0faa32d1..891d8d21dd4c 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -26,6 +26,7 @@ local error = error local str_find = core.string.find local str_gsub = string.gsub local str_lower = string.lower +local tostring = tostring local ssl_certificates local radixtree_router local radixtree_router_ver @@ -44,7 +45,7 @@ local function create_router(ssl_items) local idx = 0 for _, ssl in config_util.iterate_values(ssl_items) do - if ssl.value ~= nil and + if ssl.value ~= nil and ssl.value.type == "server" and (ssl.value.status == nil or ssl.value.status == 1) then -- compatible with old version local j = 0 @@ -261,4 +262,19 @@ function _M.init_worker() end +function _M.get_by_id(ssl_id) + local ssl + local ssls = core.config.fetch_created_obj("/ssl") + if ssls then + ssl = ssls:get(tostring(ssl_id)) + end + + if not ssl then + return nil + end + + return ssl.value +end + + return _M diff --git a/apisix/upstream.lua b/apisix/upstream.lua index d314de49d7f9..0162ad8137ed 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -333,14 +333,24 @@ function _M.set_by_route(route, api_ctx) local scheme = up_conf.scheme if (scheme == "https" or scheme == "grpcs") and up_conf.tls then + + local client_cert, client_key + if up_conf.tls.client_cert_id then + client_cert = api_ctx.upstream_ssl.cert + client_key = api_ctx.upstream_ssl.key + else + client_cert = up_conf.tls.client_cert + client_key = up_conf.tls.client_key + end + -- the sni here is just for logging local sni = api_ctx.var.upstream_host - local cert, err = apisix_ssl.fetch_cert(sni, up_conf.tls.client_cert) + local cert, err = apisix_ssl.fetch_cert(sni, client_cert) if not ok then return 503, err end - local key, err = apisix_ssl.fetch_pkey(sni, up_conf.tls.client_key) + local key, err = apisix_ssl.fetch_pkey(sni, client_key) if not ok then return 503, err end @@ -418,6 +428,29 @@ local function check_upstream_conf(in_dp, conf) return false, "invalid configuration: " .. err end + local ssl_id = conf.tls and conf.tls.client_cert_id + if ssl_id then + local key = "/ssl/" .. ssl_id + local res, err = core.etcd.get(key) + if not res then + return nil, "failed to fetch ssl info by " + .. "ssl id [" .. ssl_id .. "]: " .. err + end + + if res.status ~= 200 then + return nil, "failed to fetch ssl info by " + .. "ssl id [" .. ssl_id .. "], " + .. "response code: " .. res.status + end + if res.body and res.body.node and + res.body.node.value and res.body.node.value.type ~= "client" then + + return nil, "failed to fetch ssl info by " + .. "ssl id [" .. ssl_id .. "], " + .. "wrong ssl type" + end + end + -- encrypt the key in the admin if conf.tls and conf.tls.client_key then conf.tls.client_key = apisix_ssl.aes_encrypt_pkey(conf.tls.client_key) diff --git a/docs/en/latest/admin-api.md b/docs/en/latest/admin-api.md index 9c7bec756f64..d78eb0d6e512 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -541,8 +541,9 @@ In addition to the equalization algorithm selections, Upstream also supports pas | labels | optional | Attributes of the Upstream specified as key-value pairs. | {"version":"v2","build":"16","env":"production"} | | create_time | optional | Epoch timestamp (in seconds) of the created time. If missing, this field will be populated automatically. | 1602883670 | | update_time | optional | Epoch timestamp (in seconds) of the updated time. If missing, this field will be populated automatically. | 1602883670 | -| tls.client_cert | optional | Sets the client certificate while connecting to a TLS Upstream. | | -| tls.client_key | optional | Sets the client private key while connecting to a TLS Upstream. | | +| tls.client_cert | optional, can't be used with `tls.client_cert_id` | Sets the client certificate while connecting to a TLS Upstream. | | +| tls.client_key | optional, can't be used with `tls.client_cert_id` | Sets the client private key while connecting to a TLS Upstream. | | +| tls.client_cert_id | optional, can't be used with `tls.client_cert` and `tls.client_key` | Set the referenced [SSL](#ssl) id. | | | keepalive_pool.size | optional | Sets `keepalive` directive dynamically. | | | keepalive_pool.idle_timeout | optional | Sets `keepalive_timeout` directive dynamically. | | | keepalive_pool.requests | optional | Sets `keepalive_requests` directive dynamically. | | @@ -570,6 +571,8 @@ You can set the `scheme` to `tls`, which means "TLS over TCP". To use mTLS to communicate with Upstream, you can use the `tls.client_cert/key` in the same format as SSL's `cert` and `key` fields. +Or you can reference SSL object by `tls.client_cert_id` to set SSL cert and key. The SSL object can be referenced only if the `type` field is `client`, otherwise the request will be rejected by APISIX. In addition, only `cert` and `key` will be used in the SSL object. + To allow Upstream to have a separate connection pool, use `keepalive_pool`. It can be configured by modifying its child fields. Example Configuration: @@ -789,6 +792,7 @@ Currently, the response is returned from etcd. | labels | False | Match Rules | Attributes of the resource specified as key-value pairs. | {"version":"v2","build":"16","env":"production"} | | create_time | False | Auxiliary | Epoch timestamp (in seconds) of the created time. If missing, this field will be populated automatically. | 1602883670 | | update_time | False | Auxiliary | Epoch timestamp (in seconds) of the updated time. If missing, this field will be populated automatically. | 1602883670 | +| type | False | Auxiliary | Identifies the type of certificate, default `server`. | `client` Indicates that the certificate is a client certificate, which is used when APISIX accesses the upstream; `server` Indicates that the certificate is a server-side certificate, which is used by APISIX when verifying client requests. | | status | False | Auxiliary | Enables the current SSL. Set to `1` (enabled) by default. | `1` to enable, `0` to disable | Example Configuration: diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 7cffc2adb904..367c318ac449 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -549,8 +549,9 @@ APISIX 的 Upstream 除了基本的负载均衡算法选择外,还支持对上 | labels | 可选 | 匹配规则 | 标识附加属性的键值对 | {"version":"v2","build":"16","env":"production"} | | create_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 | | update_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 | -| tls.client_cert | 可选 | https 证书 | 设置跟上游通信时的客户端证书,细节见下文 | | -| tls.client_key | 可选 | https 证书私钥 | 设置跟上游通信时的客户端私钥,细节见下文 | | +| tls.client_cert | 可选,不能和 `tls.client_cert_id` 一起使用 | https 证书 | 设置跟上游通信时的客户端证书,细节见下文 | | +| tls.client_key | 可选,不能和 `tls.client_cert_id` 一起使用 | https 证书私钥 | 设置跟上游通信时的客户端私钥,细节见下文 | | +| tls.client_cert_id | 可选,不能和 `tls.client_cert`、`tls.client_key` 一起使用 | SSL | 设置引用的 ssl id,详见 [SSL](#ssl) | | |keepalive_pool.size | 可选 | 辅助 | 动态设置 `keepalive` 指令,细节见下文 | |keepalive_pool.idle_timeout | 可选 | 辅助 | 动态设置 `keepalive_timeout` 指令,细节见下文 | |keepalive_pool.requests | 可选 | 辅助 | 动态设置 `keepalive_requests` 指令,细节见下文 | @@ -578,6 +579,8 @@ APISIX 的 Upstream 除了基本的负载均衡算法选择外,还支持对上 `tls.client_cert/key` 可以用来跟上游进行 mTLS 通信。 他们的格式和 SSL 对象的 `cert` 和 `key` 一样。 +`tls.client_cert_id` 可以用来指定引用的 SSL 对象。只有当 SSL 对象的 `type` 字段为 client 时才能被引用,否则请求会被 APISIX 拒绝。另外,SSL 对象中只有 `cert`和`key` 会被使用。 + `keepalive_pool` 允许 upstream 对象有自己单独的连接池。 它下属的字段,比如 `requests`,可以用了配置上游连接保持的参数。 这个特性需要 APISIX 运行于 [APISIX-Base](./FAQ.md#如何构建-APISIX-Base-环境?)。 @@ -799,6 +802,7 @@ $ curl http://127.0.0.1:9080/get | labels | 可选 | 匹配规则 | 标识附加属性的键值对 | {"version":"v2","build":"16","env":"production"} | | create_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 | | update_time | 可选 | 辅助 | 单位为秒的 epoch 时间戳,如果不指定则自动创建 | 1602883670 | +| type | 可选 | 辅助 | 标识证书的类型,缺省为 `server`。 | `client` 表示证书是客户端证书,APISIX 访问上游时使用;`server` 表示证书是服务端证书,APISIX 验证客户端请求时使用 | | status | 可选 | 辅助 | 是否启用此 SSL,缺省 `1`。 | `1` 表示启用,`0` 表示禁用 | ssl 对象 json 配置内容: diff --git a/t/admin/ssl.t b/t/admin/ssl.t index 49d3ec8cd146..0232d21102fe 100644 --- a/t/admin/ssl.t +++ b/t/admin/ssl.t @@ -236,7 +236,7 @@ GET /t GET /t --- error_code: 400 --- response_body -{"error_msg":"invalid configuration: value should match only one schema, but matches none"} +{"error_msg":"invalid configuration: then clause did not match"} --- no_error_log [error] @@ -535,7 +535,7 @@ passed GET /t --- error_code: 400 --- response_body -{"error_msg":"invalid configuration: value should match only one schema, but matches none"} +{"error_msg":"invalid configuration: then clause did not match"} --- no_error_log [error] @@ -771,3 +771,72 @@ GET /t passed --- no_error_log [error] + + + +=== TEST 20: missing sni information +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + local data = {cert = ssl_cert, key = ssl_key} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: then clause did not match"} +--- no_error_log +[error] + + + +=== TEST 21: type client, missing sni information +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + local data = {type = "client", cert = ssl_cert, key = ssl_key} + + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + core.json.encode(data), + [[{ + "node": { + "key": "/apisix/ssl/1" + }, + "action": "set" + }]] + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- response_body chomp +passed diff --git a/t/admin/ssl2.t b/t/admin/ssl2.t index 752e25cd7ba4..865652ce2e89 100644 --- a/t/admin/ssl2.t +++ b/t/admin/ssl2.t @@ -71,7 +71,7 @@ __DATA__ } } --- response_body -{"action":"create","node":{"value":{"cert":"","key":"","sni":"not-unwanted-post.com","status":1}}} +{"action":"create","node":{"value":{"cert":"","key":"","sni":"not-unwanted-post.com","status":1,"type":"server"}}} @@ -104,7 +104,7 @@ __DATA__ } } --- response_body -{"action":"set","node":{"key":"/apisix/ssl/1","value":{"cert":"","id":"1","key":"","sni":"test.com","status":1}}} +{"action":"set","node":{"key":"/apisix/ssl/1","value":{"cert":"","id":"1","key":"","sni":"test.com","status":1,"type":"server"}}} @@ -137,7 +137,7 @@ __DATA__ } } --- response_body -{"action":"compareAndSwap","node":{"key":"/apisix/ssl/1","value":{"cert":"","id":"1","key":"","sni":"t.com","status":1}}} +{"action":"compareAndSwap","node":{"key":"/apisix/ssl/1","value":{"cert":"","id":"1","key":"","sni":"t.com","status":1,"type":"server"}}} @@ -172,7 +172,7 @@ __DATA__ } } --- response_body -{"action":"get","node":{"key":"/apisix/ssl/1","value":{"cert":"","id":"1","sni":"t.com","status":1}}} +{"action":"get","node":{"key":"/apisix/ssl/1","value":{"cert":"","id":"1","sni":"t.com","status":1,"type":"server"}}} diff --git a/t/admin/upstream.t b/t/admin/upstream.t index 96dcef3c482f..16bfb5157b7b 100644 --- a/t/admin/upstream.t +++ b/t/admin/upstream.t @@ -627,3 +627,136 @@ GET /t {"error_msg":"wrong upstream id, do not need it"} --- no_error_log [error] + + + +=== TEST 19: client_cert/client_key and client_cert_id cannot appear at the same time +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local ssl_cert = t.read_file("t/certs/apisix.crt") + local ssl_key = t.read_file("t/certs/apisix.key") + local data = { + nodes = { + ["127.0.0.1:8080"] = 1 + }, + type = "roundrobin", + tls = { + client_cert_id = 1, + client_cert = ssl_cert, + client_key = ssl_key + } + } + local code, body = t.test('/apisix/admin/upstreams', + ngx.HTTP_POST, + core.json.encode(data) + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body eval +qr/{"error_msg":"invalid configuration: property \\\"tls\\\" validation failed: failed to validate dependent schema for \\\"client_cert|client_key\\\": value wasn't supposed to match schema"}/ +--- no_error_log +[error] + + + +=== TEST 20: tls.client_cert_id does not exist +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin") + + local data = { + nodes = { + ["127.0.0.1:8080"] = 1 + }, + type = "roundrobin", + tls = { + client_cert_id = 9999999 + } + } + local code, body = t.test('/apisix/admin/upstreams', + ngx.HTTP_POST, + core.json.encode(data) + ) + + ngx.status = code + ngx.print(body) + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"failed to fetch ssl info by ssl id [9999999], response code: 404"} +--- no_error_log +[error] + + + +=== TEST 21: tls.client_cert_id exist with wrong ssl type +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local json = require("toolkit.json") + local ssl_cert = t.read_file("t/certs/mtls_client.crt") + local ssl_key = t.read_file("t/certs/mtls_client.key") + local data = { + sni = "test.com", + cert = ssl_cert, + key = ssl_key + } + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.print(body) + return + end + + local data = { + upstream = { + scheme = "https", + type = "roundrobin", + nodes = { + ["127.0.0.1:1983"] = 1 + }, + tls = { + client_cert_id = 1 + } + }, + uri = "/hello" + } + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.print(body) + return + end + } + } +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"failed to fetch ssl info by ssl id [1], wrong ssl type"} +--- no_error_log +[error] diff --git a/t/node/upstream-mtls.t b/t/node/upstream-mtls.t index 7af6d2e61785..c909dbc9a64f 100644 --- a/t/node/upstream-mtls.t +++ b/t/node/upstream-mtls.t @@ -77,7 +77,7 @@ __DATA__ GET /t --- error_code: 400 --- response_body -{"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"tls\" validation failed: property \"client_key\" is required when \"client_cert\" is set"} +{"error_msg":"invalid configuration: property \"upstream\" validation failed: property \"tls\" validation failed: failed to validate dependent schema for \"client_cert\": property \"client_key\" is required"} @@ -545,3 +545,145 @@ GET /t GET /hello_chunked --- response_body hello world + + + +=== TEST 13: get cert by tls.client_cert_id +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local json = require("toolkit.json") + + local ssl_cert = t.read_file("t/certs/mtls_client.crt") + local ssl_key = t.read_file("t/certs/mtls_client.key") + local data = { + type = "client", + cert = ssl_cert, + key = ssl_key + } + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local data = { + upstream = { + scheme = "https", + type = "roundrobin", + nodes = { + ["127.0.0.1:1983"] = 1, + }, + tls = { + client_cert_id = 1 + } + }, + uri = "/hello" + } + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + json.encode(data) + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + } + } +--- request +GET /t + + + +=== TEST 14: hit +--- upstream_server_config + ssl_client_certificate ../../certs/mtls_ca.crt; + ssl_verify_client on; +--- request +GET /hello +--- response_body +hello world + + + +=== TEST 15: change ssl object type +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local json = require("toolkit.json") + + local ssl_cert = t.read_file("t/certs/mtls_client.crt") + local ssl_key = t.read_file("t/certs/mtls_client.key") + local data = { + type = "server", + sni = "test.com", + cert = ssl_cert, + key = ssl_key + } + local code, body = t.test('/apisix/admin/ssl/1', + ngx.HTTP_PUT, + json.encode(data) + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + } + } +--- request +GET /t + + + +=== TEST 16: hit, ssl object type mismatch +--- upstream_server_config + ssl_client_certificate ../../certs/mtls_ca.crt; + ssl_verify_client on; +--- request +GET /hello +--- error_code: 502 +--- error_log +failed to get ssl cert: ssl type should be 'client' + + + +=== TEST 17: delete ssl object +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local json = require("toolkit.json") + + local code, body = t.test('/apisix/admin/ssl/1', ngx.HTTP_DELETE) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + } + } +--- request +GET /t + + + +=== TEST 18: hit, ssl object not exits +--- upstream_server_config + ssl_client_certificate ../../certs/mtls_ca.crt; + ssl_verify_client on; +--- request +GET /hello +--- error_code: 502 +--- error_log +failed to get ssl cert: ssl id [1] not exits From 845c3c925c3a4a841ddaf93b0826f083d9f044dd Mon Sep 17 00:00:00 2001 From: Nassos Michas Date: Wed, 15 Jun 2022 05:13:29 +0300 Subject: [PATCH 17/63] feat: Add support for capturing OIDC refresh tokens (#7220) --- apisix/plugins/openid-connect.lua | 18 +++++++++++++++--- docs/en/latest/plugins/openid-connect.md | 1 + t/plugin/openid-connect.t | 6 ++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/openid-connect.lua b/apisix/plugins/openid-connect.lua index 73427bc3311c..4a6dbda1ccec 100644 --- a/apisix/plugins/openid-connect.lua +++ b/apisix/plugins/openid-connect.lua @@ -96,6 +96,12 @@ local schema = { "header to the request for downstream.", type = "boolean", default = true + }, + set_refresh_token_header = { + description = "Whether the refresh token should be added in the X-Refresh-Token " .. + "header to the request for downstream.", + type = "boolean", + default = false } }, required = {"client_id", "client_secret", "discovery"} @@ -260,7 +266,7 @@ function _M.rewrite(plugin_conf, ctx) conf.ssl_verify = "no" end - local response, err + local response, err, session, _ if conf.bearer_only or conf.introspection_endpoint or conf.public_key then -- An introspection endpoint or a public key has been configured. Try to @@ -298,7 +304,7 @@ function _M.rewrite(plugin_conf, ctx) -- provider's authorization endpoint to initiate the Relying Party flow. -- This code path also handles when the ID provider then redirects to -- the configured redirect URI after successful authentication. - response, err = openidc.authenticate(conf) + response, err, _, session = openidc.authenticate(conf) if err then core.log.error("OIDC authentication failed: ", err) @@ -307,7 +313,8 @@ function _M.rewrite(plugin_conf, ctx) if response then -- If the openidc module has returned a response, it may contain, - -- respectively, the access token, the ID token, and the userinfo. + -- respectively, the access token, the ID token, the refresh token, + -- and the userinfo. -- Add respective headers to the request, if so configured. -- Add configured access token header, maybe. @@ -324,6 +331,11 @@ function _M.rewrite(plugin_conf, ctx) core.request.set_header(ctx, "X-Userinfo", ngx_encode_base64(core.json.encode(response.user))) end + + -- Add X-Refresh-Token header, maybe. + if session.data.refresh_token and conf.set_refresh_token_header then + core.request.set_header(ctx, "X-Refresh-Token", session.data.refresh_token) + end end end end diff --git a/docs/en/latest/plugins/openid-connect.md b/docs/en/latest/plugins/openid-connect.md index 29949107e82e..5b33e5d53ad0 100644 --- a/docs/en/latest/plugins/openid-connect.md +++ b/docs/en/latest/plugins/openid-connect.md @@ -55,6 +55,7 @@ The `openid-connect` Plugin provides authentication and introspection capability | access_token_in_authorization_header | boolean | False | false | | When set to true, sets the access token in the `Authorization` header. Otherwise, set the `X-Access-Token` header. | | set_id_token_header | boolean | False | true | | When set to true and the ID token is available, sets the ID token in the `X-ID-Token` request header. | | set_userinfo_header | boolean | False | true | | When set to true and the UserInfo object is available, sets it in the `X-Userinfo` request header. | +| set_refresh_token_header | boolean | False | false | | When set to true and a refresh token object is available, sets it in the `X-Refresh-Token` request header. | ## Modes of operation diff --git a/t/plugin/openid-connect.t b/t/plugin/openid-connect.t index a97898d6e8e5..22786eaea9f2 100644 --- a/t/plugin/openid-connect.t +++ b/t/plugin/openid-connect.t @@ -189,7 +189,8 @@ true "set_access_token_header": true, "access_token_in_authorization_header": false, "set_id_token_header": true, - "set_userinfo_header": true + "set_userinfo_header": true, + "set_refresh_token_header": true } }, "upstream": { @@ -272,6 +273,7 @@ user-agent: .* x-access-token: ey.* x-id-token: ey.* x-real-ip: 127.0.0.1 +x-refresh-token: ey.* x-userinfo: ey.* --- no_error_log [error] @@ -916,7 +918,7 @@ OIDC introspection failed: invalid token --- request GET /t --- response_body -{"access_token_in_authorization_header":false,"bearer_only":false,"client_id":"kbyuFDidLLm280LIwVFiazOqjO3ty8KH","client_secret":"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa","discovery":"http://127.0.0.1:1980/.well-known/openid-configuration","introspection_endpoint_auth_method":"client_secret_basic","logout_path":"/logout","realm":"apisix","scope":"openid","set_access_token_header":true,"set_id_token_header":true,"set_userinfo_header":true,"ssl_verify":false,"timeout":3} +{"access_token_in_authorization_header":false,"bearer_only":false,"client_id":"kbyuFDidLLm280LIwVFiazOqjO3ty8KH","client_secret":"60Op4HFM0I8ajz0WdiStAbziZ-VFQttXuxixHHs2R7r7-CW8GR79l-mmLqMhc-Sa","discovery":"http://127.0.0.1:1980/.well-known/openid-configuration","introspection_endpoint_auth_method":"client_secret_basic","logout_path":"/logout","realm":"apisix","scope":"openid","set_access_token_header":true,"set_id_token_header":true,"set_refresh_token_header":false,"set_userinfo_header":true,"ssl_verify":false,"timeout":3} --- no_error_log [error] From 3b0a51f46ae31fd7b12785d2df5ec4dc5b648b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Wed, 15 Jun 2022 10:42:30 +0800 Subject: [PATCH 18/63] docs: correct the repo url (#7253) Signed-off-by: spacewander --- docs/en/latest/health-check.md | 2 +- docs/zh/latest/health-check.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/health-check.md b/docs/en/latest/health-check.md index 850a96e60e2d..3f16d09309bf 100644 --- a/docs/en/latest/health-check.md +++ b/docs/en/latest/health-check.md @@ -23,7 +23,7 @@ title: Health Check ## Health Checks for Upstream -Health Check of Apache APISIX is based on [lua-resty-healthcheck](https://github.com/Kong/lua-resty-healthcheck). +Health Check of Apache APISIX is based on [lua-resty-healthcheck](https://github.com/api7/lua-resty-healthcheck). Note: diff --git a/docs/zh/latest/health-check.md b/docs/zh/latest/health-check.md index 592218386fb1..3cd1e7789615 100644 --- a/docs/zh/latest/health-check.md +++ b/docs/zh/latest/health-check.md @@ -23,7 +23,7 @@ title: 健康检查 ## Upstream 的健康检查 -Apache APISIX 的健康检查使用 [lua-resty-healthcheck](https://github.com/Kong/lua-resty-healthcheck) 实现。 +Apache APISIX 的健康检查使用 [lua-resty-healthcheck](https://github.com/api7/lua-resty-healthcheck) 实现。 注意: From 851adc2e86b2d7be122b5dd9e4adf5915a55f463 Mon Sep 17 00:00:00 2001 From: soulbird Date: Wed, 15 Jun 2022 12:47:57 +0800 Subject: [PATCH 19/63] fix(benchmark): write worker_processes into config.yaml (#7250) --- benchmark/run.sh | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/benchmark/run.sh b/benchmark/run.sh index 7d1f06a67d7b..8bb1047fba17 100755 --- a/benchmark/run.sh +++ b/benchmark/run.sh @@ -35,12 +35,15 @@ mkdir -p benchmark/fake-apisix/logs make init +fake_apisix_cmd="openresty -p $PWD/benchmark/fake-apisix -c $PWD/benchmark/fake-apisix/conf/nginx.conf" +server_cmd="openresty -p $PWD/benchmark/server -c $PWD/benchmark/server/conf/nginx.conf" + trap 'onCtrlC' INT function onCtrlC () { sudo killall wrk sudo killall openresty - sudo openresty -p $PWD/benchmark/fake-apisix -s stop || exit 1 - sudo openresty -p $PWD/benchmark/server -s stop || exit 1 + sudo ${fake_apisix_cmd} -s stop || exit 1 + sudo ${server_cmd} -s stop || exit 1 } for up_cnt in $(seq 1 $upstream_cnt); @@ -55,14 +58,26 @@ do done if [[ "$(uname)" == "Darwin" ]]; then - sed -i "" "s/worker_processes .*/worker_processes $worker_cnt;/g" conf/nginx.conf + sed -i "" "s/\- proxy-mirror .*/#\- proxy-mirror/g" conf/config-default.yaml + sed -i "" "s/\- proxy-cache .*/#\- proxy-cache/g" conf/config-default.yaml sed -i "" "s/listen .*;/$nginx_listen/g" benchmark/server/conf/nginx.conf else - sed -i "s/worker_processes .*/worker_processes $worker_cnt;/g" conf/nginx.conf + sed -i "s/\- proxy-mirror/#\- proxy-mirror/g" conf/config-default.yaml + sed -i "s/\- proxy-cache/#\- proxy-cache/g" conf/config-default.yaml sed -i "s/listen .*;/$nginx_listen/g" benchmark/server/conf/nginx.conf fi -sudo openresty -p $PWD/benchmark/server || exit 1 +echo " +apisix: + admin_key: + - name: admin + key: edd1c9f034335f136f87ad84b625c8f1 + role: admin +nginx_config: + worker_processes: ${worker_cnt} +" > conf/config.yaml + +sudo ${server_cmd} || exit 1 make run @@ -140,7 +155,7 @@ else sed -i "s/worker_processes [0-9]*/worker_processes $worker_cnt/g" benchmark/fake-apisix/conf/nginx.conf fi -sudo openresty -p $PWD/benchmark/fake-apisix || exit 1 +sudo ${fake_apisix_cmd} || exit 1 sleep 1 @@ -150,6 +165,6 @@ sleep 1 wrk -d 5 -c 16 http://127.0.0.1:9080/hello -sudo openresty -p $PWD/benchmark/fake-apisix -s stop || exit 1 +sudo ${fake_apisix_cmd} -s stop || exit 1 -sudo openresty -p $PWD/benchmark/server -s stop || exit 1 +sudo ${server_cmd} -s stop || exit 1 From 9e7d2535b68f3f03314daca4d50fbded09b4c2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 16 Jun 2022 09:43:06 +0800 Subject: [PATCH 20/63] feat(deployment): add structure of traditional role (#7249) Signed-off-by: spacewander --- apisix/cli/file.lua | 8 ++ apisix/cli/ngx_tpl.lua | 12 ++- apisix/cli/ops.lua | 4 + apisix/cli/schema.lua | 101 +++++++++++++++--------- apisix/cli/snippet.lua | 65 +++++++++++++++ apisix/core/config_etcd.lua | 4 + conf/config-default.yaml | 11 +++ rockspec/apisix-master-0.rockspec | 2 +- t/cli/test_deployment_traditional.sh | 113 +++++++++++++++++++++++++++ 9 files changed, 281 insertions(+), 39 deletions(-) create mode 100644 apisix/cli/snippet.lua create mode 100755 t/cli/test_deployment_traditional.sh diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua index 66600b54b41b..5bd64a682e96 100644 --- a/apisix/cli/file.lua +++ b/apisix/cli/file.lua @@ -237,6 +237,14 @@ function _M.read_yaml_conf(apisix_home) end end + if default_conf.deployment + and default_conf.deployment.role == "traditional" + and default_conf.deployment.etcd + then + default_conf.etcd = default_conf.deployment.etcd + default_conf.etcd.unix_socket_proxy = "unix:./conf/config_listen.sock" + end + if default_conf.apisix.config_center == "yaml" then local apisix_conf_path = profile:yaml_path("apisix") local apisix_conf_yaml, _ = util.read_file(apisix_conf_path) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index cf8c08b38bb7..161c530b8d74 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -64,8 +64,9 @@ lua { {% end %} } -{% if enabled_stream_plugins["prometheus"] and not enable_http then %} +{% if (enabled_stream_plugins["prometheus"] or conf_server) and not enable_http then %} http { + {% if enabled_stream_plugins["prometheus"] then %} init_worker_by_lua_block { require("apisix.plugins.prometheus.exporter").http_init(true) } @@ -88,6 +89,11 @@ http { stub_status; } } + {% end %} + + {% if conf_server then %} + {* conf_server *} + {% end %} } {% end %} @@ -570,6 +576,10 @@ http { } {% end %} + {% if conf_server then %} + {* conf_server *} + {% end %} + server { {% for _, item in ipairs(node_listen) do %} listen {* item.ip *}:{* item.port *} default_server {% if item.enable_http2 then %} http2 {% end %} {% if enable_reuseport then %} reuseport {% end %}; diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 0be0701f6827..1e27d9a206d5 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -21,6 +21,7 @@ local file = require("apisix.cli.file") local schema = require("apisix.cli.schema") local ngx_tpl = require("apisix.cli.ngx_tpl") local cli_ip = require("apisix.cli.ip") +local snippet = require("apisix.cli.snippet") local profile = require("apisix.core.profile") local template = require("resty.template") local argparse = require("argparse") @@ -538,6 +539,8 @@ Please modify "admin_key" in conf/config.yaml . proxy_mirror_timeouts = yaml_conf.plugin_attr["proxy-mirror"].timeout end + local conf_server = snippet.generate_conf_server(yaml_conf) + -- Using template.render local sys_conf = { use_openresty_1_17 = use_openresty_1_17, @@ -557,6 +560,7 @@ Please modify "admin_key" in conf/config.yaml . control_server_addr = control_server_addr, prometheus_server_addr = prometheus_server_addr, proxy_mirror_timeouts = proxy_mirror_timeouts, + conf_server = conf_server, } if not yaml_conf.apisix then diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua index 7afece3ab239..ab053a0a727e 100644 --- a/apisix/cli/schema.lua +++ b/apisix/cli/schema.lua @@ -22,6 +22,43 @@ local require = require local _M = {} +local etcd_schema = { + type = "object", + properties = { + resync_delay = { + type = "integer", + }, + user = { + type = "string", + }, + password = { + type = "string", + }, + tls = { + type = "object", + properties = { + cert = { + type = "string", + }, + key = { + type = "string", + }, + } + }, + prefix = { + type = "string", + pattern = [[^/[^/]+$]] + }, + host = { + type = "array", + items = { + type = "string", + pattern = [[^https?://]] + } + } + }, + required = {"prefix", "host"} +} local config_schema = { type = "object", properties = { @@ -190,43 +227,7 @@ local config_schema = { } } }, - etcd = { - type = "object", - properties = { - resync_delay = { - type = "integer", - }, - user = { - type = "string", - }, - password = { - type = "string", - }, - tls = { - type = "object", - properties = { - cert = { - type = "string", - }, - key = { - type = "string", - }, - } - }, - prefix = { - type = "string", - pattern = [[^/[^/]+$]] - }, - host = { - type = "array", - items = { - type = "string", - pattern = [[^https?://]] - } - } - }, - required = {"prefix", "host"} - }, + etcd = etcd_schema, wasm = { type = "object", properties = { @@ -255,8 +256,25 @@ local config_schema = { } } }, + deployment = { + type = "object", + properties = { + role = { + enum = {"traditional", "control_plane", "data_plane", "standalone"} + } + }, + required = {"role"}, + }, } } +local deployment_schema = { + traditional = { + properties = { + etcd = etcd_schema, + }, + required = {"etcd"} + }, +} function _M.validate(yaml_conf) @@ -279,6 +297,15 @@ function _M.validate(yaml_conf) end end + if yaml_conf.deployment then + local role = yaml_conf.deployment.role + local validator = jsonschema.generate_validator(deployment_schema[role]) + local ok, err = validator(yaml_conf.deployment) + if not ok then + return false, "invalid deployment " .. role .. " configuration: " .. err + end + end + return true end diff --git a/apisix/cli/snippet.lua b/apisix/cli/snippet.lua new file mode 100644 index 000000000000..014719511faa --- /dev/null +++ b/apisix/cli/snippet.lua @@ -0,0 +1,65 @@ +-- +-- 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 template = require("resty.template") +local ipairs = ipairs + + +-- this module provide methods to generate snippets which will be used in the nginx.conf template +local _M = {} + + +function _M.generate_conf_server(conf) + if not (conf.deployment and conf.deployment.role == "traditional") then + return nil + end + + -- we use proxy even the role is traditional so that we can test the proxy in daily dev + local servers = conf.deployment.etcd.host + for i, s in ipairs(servers) do + local prefix = "http://" + -- TODO: support https + if s:find(prefix, 1, true) then + servers[i] = s:sub(#prefix + 1) + end + end + + local conf_render = template.compile([[ + upstream apisix_conf_backend { + {% for _, addr in ipairs(servers) do %} + server {* addr *}; + {% end %} + } + server { + listen unix:./conf/config_listen.sock; + access_log off; + location / { + set $upstream_scheme 'http'; + + proxy_pass $upstream_scheme://apisix_conf_backend; + + proxy_http_version 1.1; + proxy_set_header Connection ""; + } + } + ]]) + return conf_render({ + servers = servers + }) +end + + +return _M diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua index f022fa96552e..8736059f7bf5 100644 --- a/apisix/core/config_etcd.lua +++ b/apisix/core/config_etcd.lua @@ -816,9 +816,13 @@ function _M.init() return nil, "failed to start a etcd instance: " .. err end + -- don't go through proxy during start because the proxy is not available + local proxy = etcd_cli.unix_socket_proxy + etcd_cli.unix_socket_proxy = nil local etcd_conf = local_conf.etcd local prefix = etcd_conf.prefix local res, err = readdir(etcd_cli, prefix, create_formatter(prefix)) + etcd_cli.unix_socket_proxy = proxy if not res then return nil, err end diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 8f9e58e5df17..c33e4a731231 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -477,3 +477,14 @@ plugin_attr: send: 60s # redirect: # https_port: 8443 # the default port for use by HTTP redirects to HTTPS + +#deployment: +# role: traditional +# role_traditional: +# config_provider: etcd +# etcd: +# host: # it's possible to define multiple etcd hosts addresses of the same etcd cluster. +# - "http://127.0.0.1:2379" # multiple etcd address, if your etcd cluster enables TLS, please use https scheme, +# # e.g. https://127.0.0.1:2379. +# prefix: /apisix # configuration prefix in etcd +# timeout: 30 # 30 seconds diff --git a/rockspec/apisix-master-0.rockspec b/rockspec/apisix-master-0.rockspec index 9fc43aaa07bf..f5bfdae206f7 100644 --- a/rockspec/apisix-master-0.rockspec +++ b/rockspec/apisix-master-0.rockspec @@ -34,7 +34,7 @@ dependencies = { "lua-resty-ctxdump = 0.1-0", "lua-resty-dns-client = 6.0.2", "lua-resty-template = 2.0", - "lua-resty-etcd = 1.6.2", + "lua-resty-etcd = 1.7.0", "api7-lua-resty-http = 0.2.0", "lua-resty-balancer = 0.04", "lua-resty-ngxvar = 0.5.2", diff --git a/t/cli/test_deployment_traditional.sh b/t/cli/test_deployment_traditional.sh new file mode 100755 index 000000000000..f6d7d62c981b --- /dev/null +++ b/t/cli/test_deployment_traditional.sh @@ -0,0 +1,113 @@ +#!/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 + +echo ' +deployment: + role: traditional + role_traditional: + config_provider: etcd +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'invalid deployment traditional configuration: property "etcd" is required'; then + echo "failed: should check deployment schema during init" + exit 1 +fi + +echo "passed: should check deployment schema during init" + +# HTTP +echo ' +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - http://127.0.0.1:2379 +' > conf/config.yaml + +make run +sleep 1 + +code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1') +make stop + +if [ ! $code -eq 200 ]; then + echo "failed: could not connect to etcd with http enabled" + exit 1 +fi + +# Both HTTP and Stream +echo ' +apisix: + enable_admin: true + stream_proxy: + tcp: + - addr: 9100 +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - http://127.0.0.1:2379 +' > conf/config.yaml + +make run +sleep 1 + +code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1') +make stop + +if [ ! $code -eq 200 ]; then + echo "failed: could not connect to etcd with http & stream enabled" + exit 1 +fi + +# Stream +echo ' +apisix: + enable_admin: false + stream_proxy: + tcp: + - addr: 9100 +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - http://127.0.0.1:2379 +' > conf/config.yaml + +make run +sleep 1 + +if grep '\[error\]' logs/error.log; then + echo "failed: could not connect to etcd with stream enabled" + exit 1 +fi + +echo "passed: could connect to etcd" From d57ac67ad8dcc8f1618cd4a2a62ff60802c7d9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 16 Jun 2022 10:07:30 +0800 Subject: [PATCH 21/63] docs(deployment): sync design to online docs (#7256) * docs(deployment): sync design to online docs Signed-off-by: spacewander * Apply suggestions from code review Co-authored-by: Sylvia <39793568+SylviaBABY@users.noreply.github.com> * change image Thanks for @SylviaBABY Signed-off-by: spacewander * change Signed-off-by: spacewander Co-authored-by: Sylvia <39793568+SylviaBABY@users.noreply.github.com> --- docs/assets/images/deployment-cp_and_dp.png | Bin 0 -> 59955 bytes docs/assets/images/deployment-traditional.png | Bin 0 -> 48338 bytes .../architecture-design/deployment-role.md | 137 ++++++++++++++++++ docs/en/latest/config.json | 3 +- 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 docs/assets/images/deployment-cp_and_dp.png create mode 100644 docs/assets/images/deployment-traditional.png create mode 100644 docs/en/latest/architecture-design/deployment-role.md diff --git a/docs/assets/images/deployment-cp_and_dp.png b/docs/assets/images/deployment-cp_and_dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6445cb3fd2c57d10b9255491f6665bed292e658c GIT binary patch literal 59955 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 z zq=gr^NepZyt&;s?!XP#*9Lkt0clnzi2RJW{h836I7m6iEns1}?zm=yC1D>D`?= z6Ym-H;yBN3+3sfS9PA#st#b0Wb9+~RNf!SsPCBpGwHx5dON^TCtZf*t_KT`T_STN* zze~++vYPJm4sh@3?Xb-!iSB>+fHiaQ(F~=R=V-Ct6&z}c zZY3y*WOn|+gE}4}s2P*0xf{+k$cG2JF$PXMKyoHO_Q*Rp033jo&Y%L04!xZi0V}NE z2T(0`@a9F1vB4Z!gB~bY=^XbMc4Wz?S3dI%sAW_FV(T`=g99~DK=Ox{chr){lQ_zC z5$mHA;td=OAOFnpEeBI*W!@oD8+PA((=onw)1{t;!a#|7gfI6*@vJLwQlf&xhfPXrMnMqWbd1!YYUy@{ zMC~V9^37_BGazCtF&AkJV?&rO7N8ry@UT(kr@!|*D&qLI?szb7vT!QK?qNIge`F2UE_1PYONrtWdXDsdML!rM`LJLbp-chC$%N zsOI~EHMbT)8Clo|_44~&o7)NlhiV5|1#TdcV2`Rt=-vF7qXio&j{j&NLFm1;y40;q zv2TZar#%?8NhQeTs=(-ts<}mX6ijAGA~nVU%VKv+>x)*d%~snI0h@YdAp38SB-&I5qisr?|Qj60a7nsbTQv@Yb0mHBV`c%6e6`FnE`e#k87Z4kH ziSN0}*FnLZYmW7KB8g`p;>N9CrXZ?SVs$aAcm=)x<~{WJgujPkKe9*S!3#$ynhQ)Ty@12 zEO+|CA4So@6T;c19dgH?)!)@O=jpGvNOr2uIdzYF&XO{wtL?FcUm=;-!#C4Mh`V9X zvtqnY!Xv>(W(G-V$4#Jxgqmq#JX4mw>u;cQrR1iT)y_m3n5#Xu$)pfda9&v&0?KJ3Ax=xlh&&`L;t-fG(6$V}!Zw zi&*x<3AKW(Y#j8vB64D=FpKO#&poB)8jexI_r`yOoAZ6{-?K14fb zB6U{a><;~n2)wVJ-8KmB2;MgpxSIxqb$tVp^~mN4GD#-RPle7t9OyRjH?n#k`kBX_ z8`$%J(u#*=LNXeSml4(Cd^V@3_nygeg2#C2TFCN(x!!@a1jE~0&GC<$o}>1K6t`wD zFEM^D%tW7lGq$C7qh7xfenTd@g>P2 z?P==PV$-J`_A0t3;~;t2H!dH0OAUwLJGsF$@i2-Vk#}3*2{m8VsFxV&#rO4yF)t&b?4!bsc? z5=z*KP@MLQ@?UXTj0NFkTOSJ53W?j1uV)G99gJIsWypMLwgm9yR5mBZi^iVtJ4oE< z?`A}240Y*>cP5G@zAqiZo6CAJh8j_+0k~5=c>a5u!p3p5Ib@NAxg*&6a6)- zVppp6K@0topDO=Q<<)LXgYTryz5BybOFGhbR?)_$Z)rXPsFZ>>F80t6l}8u5?ple| z0Ap;+xG=KR?b2?U>}z4-t~{#~t@1XQ*>%#c9$_Emd$nTPQJvzpC0ZPDqw~Pvg}7e( z-qx7BRMSD8Nj=$o`0Ic^-+PwU@j`-dwW z&EIro?KYewcPku8)-iN+iGmrO4c!T3BHKSMT}j)eyQe9$yH=Q=jca&LD-g$>mhcN5 zTRsP`h?n}nCl>!61HTBK{HPF4F@p&zyGsz7Hz|jLkkr{VR3t7x0%^rsuc>jqQb5}M zd+FDCk=HNV25@g2Eq{dhILIy8Gk$SUFC~dsF%&%GVd2z=h4S!GtGXfMqF<5Xb1LL_ zZDXukYK9qh&DD0+?F0jE1yfG|)p@uggkVtv!@{mOsYVLsbolY>ydwOSZl13`U#_+H zeNjRlfkA%atvkU^V~3RtC7&%^tgUq>Nrf6ZkE4ru{j+nLdH^tJ#j9=)QHG*$BQR(4 z2Dh@@Mhq>oZnNA}gwSj z*+@xLCo-&UQX-c^T<5!Hsq(SFU9NF&0VMyjttY}+>(bz_}MA56mw zXvzbaJzT2OL34f=J&X5NeSI@txeEqwe3>y0n^i3w7bG^dORk4qP@6boC;QM;9>3VdpWR zLLh**n0&awS{Tfe@2pp1@tOLzcrkEufx{uUPGqa{4Hxf}q3`O)gzbuYqrnsE>IgCj zCV(LSPA1eadZ;2@iM;N`&J^I1!trtN|*XqS9#=A9x0vckYKU+1BceW48(!|kbO z^0PCrgpWDM5ltrUO6vsthun ziMwWqZ1>0o)^A1Et|rj$@rpSz!r&srGP*Z?W#L57jhBa@cv|SEEX(cJd5A_VKS95d zC;Jg_-?csAjmujmK8tUGWF*4oI~D~5+%^vo5LJrf1+2OZtM`?1)6iFbi3Lc8`%3cHCBKfstmJvlzE*iv=d)RoIS7F732Ivh2IV#a zxb_HK35A-`*Gr@CrT7Z|9 z!aKvu7!8HRUio(7wr05_kE*MYu>3lKt3S#^Dx}2O&fW=rC{&7<^{T=DBFVDL(fb_V z9UT)|NlegSg6CfQv%#EdsW)#NcGk75?5C*Y)1j;heT`5MxFa6D>=#Ev&~=)YfRA%0 z0&$vfrhUJDZ|S`EMUwy{K{EM)U62?LvdR^fuxvd;jXyI|wGK!cd^Q*2V`H&$teLO9 z759LM=vz>62DE9c>n5X)S$jY-9Gmk|#hzX`fQk+j;>w*+wAi%Qc582hq&YET8Jt^- z`=GG^ZfRHRciD}nveI;p*}HJbX}Dm~J%Dey@s`wnqs6-8P}a%#&T<{du1a2X+c^6o zWg%?`WLL7bzx(9M)XU8_EO12p%%q#zjRWZECWpXo&#|jJ7=z0oiGzsz;q8-LsJNTw zxfWrtQEozgJu@?R=b;sMmTr|{4X4mEdU0kN-I56n?+M~U3C~$pM$4y?DuSJ7y@H(Q zV@e{Y^t>cqFMRFLG$6f9b1i%OaCnHslmv@vYsM4}RRGx#6zUsLTJ`z&aq9Fh+j?wZdzikx zL?C8EmUG){8TK$wM(?cQdBgfA>bzK&d9JV}?36faUUPPulf={T!Sro=I2Od)NJQvV z)MAdxvjJX~#k$EnvaDvu!?9ZzXK}ewOjYlD$=C|P`L_ARE#X^>(_`;uBP;gi9TTK( zhk$Yo`V96!_BRVJB572MDDtI=lXqgxaQk@ogpat6mc-g9gFS+SFZEE2&%Ic71+|2inrcgCr*xca!)Y5 zj*5l?B+F0`^Yrv|h2yZ0Lvy}C#hiriK&!5APbf~t02YxUK#Yn8<#;tf0E`FJ(5LtA zfXv z?V9J5fj)~W+urde2k7(Yy%ns3{btT7%Cnz-c#l~V^Pb`**Y+C# zwE%mh8b~|XnV@DKW{lhWEMmN0Q+!cfmRDZW*&P$h2z)4w1HvE;CS03~Q>2(P>Z2m5 zeGZ2?ebg!E9s_za0FU?HVlK5Gzb=QtFzl5*cH@MY_32t%Og)N*vX}Q zClWXp2v9~9cVs4SOW9R^uzJ~lt;WA>N@uU}eT_k>RGcNq!zELd+H7ylT{==ejVrMa zma+FlW_o(QUg`UWEs$8=0tz_l8vAICQJ|+Sco^XA?LDLTqM*heX*`wK1POoq_?WJ|oSapiG-MO%T9 zXIydR*uI4V47WWx%`L3eO};zM}`*9f0%2Bw`<>esh}_7Rro0? z^zB!AIYJyLRMiITdFk48GDs!Kn6G6gH&pY_qk~erCKLY$` zb~3F9Z3qIy2EEq{%~+Ka?g6Nm^H{AO6OP0YR*gvCYs_mp`>X%myQ=^TEo|SHcO#sZ z%c#0XJhwTOC4srfXKRW0X6?b|uwAgrTz2Nu3!<(cLaBhkwah^3A~jh4Ih?`2UBGR| zhn$}5w$lvIQBnEWN~0VKxzsAEejEBj1JSvI6;@Ur74S)B+0ob?Fpm^8$8e4oo6F-v zUh0-u++2X*+QqGOh%IAPs`!h;VjqN)CnP5N0D<9TY>taDpmxoM3>yl@9tE*~M4WbM zJBu5k9v0a)H)moH5VV~Ie~W;NV#xzUD?kV|?gP*+wn@(aG^arU@dG%yfsRKhX;^V5 zP@XF`6=%MXW-keO#MFZ@&3xriVotrO{f9L?MuKC-t&k6)JuoSL{;!gE>imUgft2PD zXEbl&j|2aJnO5C5ZmzDMA{nKuJCdG1e-6-f;u~lgyF^p+H=&JSN%h#lX`9oPup82^ zUFi}KxQ+Ti7pnmhWpX5Jl&TYwg5;wn<{fAAHSw_p(?Ifg4Jbm6mRNql^qV-7Z+v@5 z@o6UX%r9~@?9)f(LDT@1T&ku@$yGPwFlN>FJ)vP5UUoyif!pUoZu8UGU6bK&Kn!@< zo(cr^&43x-<^T}FoGp)zEI$2>$0*N&z2YN&WxPRB5?=Mgb?ZG}Nj6G_d%x;3Q#vnS zqgYAE<4&Q;5{Hx{9`Q^?)GeAnL!iiU58`(~3L*LakFcS&1+Y_FQJ3=d%AFa-1m@+< z&??vxmc`%*yV-vh+o^nY&AodT#CFC$6DIk%62=V=jC2$E$$^5;`yN)>13~p{6dqaTH>1`79`d;g`e$ie{O^pxeq2>1nJ+Qhu+#;SAR)BzSa|u;j0$KF@ zi;e9g91G-9SyT1Vjj<+I-14h3l>k(dYppIm4hlxC5uOd)FL7m~ zv^f_DP>@`Jn`IA`KyErvbSI(q8_2wURcwcg0KIu->Zn(*@*`m>D?q!X;pg*PZDkMS zFWe2PV-AZW-u79TGd?g^qPYH@uoZ z$Rt4IW3=rGVOd@ysJLfQP7na)Yal|8I$ooaBctCJPfhF|+t(isGbVHX|kh|VE$=%@M%Xtb??O~~z z&1U!s%4OD(B${D@RkaNvJ@*`Cef{mLD_DW_c&4PD8aJ?8hN)1!D!^4=xwn+$H#d8O zM>2WlV$XOnM9QI^Bm&aHBq-7wfxDWXDW)Av?QDem_R0*kp?(GIf)T{qHwRSXgF<;5 zI4F^;4m98+jzZaJis?hVUAl3EfjK%a=8@;E2Z)!r^e=0WL%85Y9*dyN{#umN#HCQk zjBM&x!A5i@qky>Ey?355Hi5&TG9OYJZ|~rrfBg78M@)2rT_JnBqkZO0V6DvYeet$( z#eZ%+mGDy$Pr=95)2qyApw_~;dEMtU-)0+@I}B#2rERzhcHIuy6_ZE&s_Cx|bU)A9 z9460C34aEh2@bIzY=WsOTgq{!RO$Ko7tN=Z zyGM`fokK`w-sW7>=l4LpcYY`<>NlFzx2R}tlHfjkG0LVN2e=`jP|TIYVu30 z{e$_9cMr-N3O*ZFT}-PL@14kahG3(b?K_R$J0ebQs7$@skY~t`A52C}sh_K&7hfl) zd7z+E2PdxeJI4<}jsGn5N{n0=yp za>-1Yldy6G7*Ir}${|CIT}ERi>rX`Yx2l6}vg6`LIa_bV!1 zu~0GNVw`5XSo+Tt6mXy4+oX_)GMBffxB&;Um8a3nF$o4=e8o?E^Pbh1Im=FLgdD+h6^~^xxm)}b1blVwSW;G=ss1_ez=Z)Fj zG?*Xk9j*79TBhLfIvRyhbSi=~fN7`4;?+8>qig>3BD!k%Xzz?*(u-%Y)_|}KiM)8X zD`C4F-|h<&kcOj$KXKBCD#G6gjn2ERw<-D@TgI6xC5IHfEqE{l&MC^*+?R1d+11m| z^-OI-S>lBup^94`F?tOtM@X)kMXDH9F)=X*GEf$W`UpLf>0E4n7bHeQQ*n;&r-k0c zs!1dG@y)i~a{!G=ozIp~*Z4-to(}1#)T6tQ1h1+AiBDk07wPPIjJoA-9f3;^G^M+9 zn79?tg}ta#0#v^Ad_w@TnttklY4N3;%lpS~|L;gkhFKQ-e+C z#H(v--H*e?X(ri8nP+~UFVgBXT0YVkUCQg}DWA2aFDdCRDZS-3>@?GJlOMm27NY7W z=DCztQ?13>(M07^hSPcaY5jhZ??sZ8Na1IN4DRwcffm%EUhqcbY|~8%{0#>8uGAZ( zYdtR$ZFVL%jj?6+uUNW#aU)b}1h>CNrS|mRP|FYgc?r;teNcb{d3L$W8i3OHgC-cG ze;961Y7o8)%=`ZR6u!K z)eCHRS{f%%_KZyz0*REirdE*UnOm^HRSrryKkylnBnG*A5y0(KxS_oW)OW~!sbeLL zD7H3}meOVxfmC6?Clp94B10YsHkBS`gof!%pQFFpOK^;Ar)z0xK}FnMKgW(6NK1{D z(mg13EPjujFW`1>e0`fI$Rh8c4i?Jtut>rWiGcs3rxIs_FtlP`+Arw}>SoKv?dMf^ zNxl>=OJG|IC~BjR(~W&HbP}bG7nyP*@2%v2qsxH~q!V1-i+@JMhowIuDm~y@!6Yz4IXGJW!!7lO?fCI&X;fcK z0AXq~T&~9*O_Vlq$iq8x$*iOEu#6kr*x8t z^6+LiGhB_E76bU>n*rR3;nRbyBAbK;pMW=>8{fXpZ>E4xy8X*SGNpYTH(@^IEx zjS$Hv_G5KTl@&Y*TpGCiP$JIbWBO_B#wY3pR-Y5JRh98L9)eQI^}Pe7*u)~8=*P{k zkY6U%XOxdddN}37VGv$H#E8))4y;5?4?ujPR2YPgnVv3XAQ^7mzk0LnmVEv{fnN9& zWaHaoQ9go`^aNO^0PYX^{GszLise7|=GWB+L(r|ZFj4H_{_FT0U#&8O>F!^EFZd6G zg-Gb8__0niF!pYZ-fCdaW@ zZs;?tVKUdedXhCg$_3OcfO0#zM}LH}L5nukpsp-qxR~|twurEjKHs^|(fsC%{imO( zS!`{xQ}gS;9+dD>68Nd08Vk*{|31mTo>T^wBeI&_=XktN3=ilQ6moj$x!N74w%447YpX(vVnOa@R3alcFSA9>l|A5CLt z)zaJPw!dZ-n4a*)K)P2Ne+{}z0hm?O#y%Z@L+kgCfUCwpFm3;bG=S`{%It_|27VGY zGCUXOHYt1J>A^VD;$m=$|5}&e5Of-ZZIpCgxczN#Q4GMY{{Q=q6_&4`UAVCZEdqjt zF!kR~F6#M%cM41ZP`&p5(mo+8D`$=j_OtvGOlW9Ie)QjH=LtD|a$12U@LUV5WkbV3 z>JZ}$@2vA~zeq|8_ubXTjW#30HBwK8?67;lUG_%Pz*ToWD=vRcDOS?DwxClmbCcytc>UGD1zHmWTXT^!8rF|b|hF9$Eu{B-weXXi!f9NyM=mN zs;kct5(GWMX%huxnEr*A1^;6Ik!u9>U_UahYF04L+rjTV2tF0z?^CKBw&K-IDm)r@PP~Tk2MVUg4$3;t@J+!q>}+eqQ8Im*bbra z0eMXs+~Xld@5(dCS$b4cmc=iLpWkMx2y2cv{lLI|sQ2$-_~U+?yI|nBRK|+%gx+8Y zN?}$vvB$^X_ksi7BCsIk2A2#UgXTRyXYu}Va6r$2PxTK@_vi;!ACq8eiwOPofIZl) z-6L^(Oxg{(T`$@52eTMus0Qf3UGF`0unrgeCM{<-P#?nB@dD^FJOB?gXv9t3l17 zo(q^!ZHDtcM$QR?RxHi%Aa6;qM}#pEzjx|l%2=9|e@BIY7?5Q(abE+!x9~vC=)VuZ z6}$tyD@tp+H!*W@a=4rUMsH|w-&!2AiTrVh!;pO7r7Uv!{%#QPttCqZ08c(q$Z>5C zC>(}dz!2?Up3L4YDXQbftr#B7*2t{)+k1MJ&kUf?2^j918dVMcDG+n%6_S&ZQ1;-> z6o~0~t^KTWo0hAyAeMz!pCHquA2nm=8iSr?KFKc*9?RxN;7kQ!9PgcXWwu}MDgLj> zG0T2xg9{^zjJW?LygJwC+%|*oU)x3_a9=*4c*I?znLw? zzB4F}xM`|>dkwhKK@jL3u^o)pZK@5#hFF(oRoAb90X!zA{dc&bNi3A@dU(Hptl@M# z^&lA-1`%1bQmkC><;ppKOnix7bPtbpgkAN{A8!Ly3{fM2f}MjQeFKqB+bAZ1x79*AnT#e@i`)T{XSWT_D;BHmIJ;kP+DINADKK56~Hl#Yj{QWEMGn>~^}MFxAR#GMq*|LdF9 z#4dgREi*!R)CNTDEHi>E2jpKYEiW74JwNogxl7%{i)&oizv#jFY=iX+(Mg{G$&U|y z+NH{{gaiudIP){_g%Wa4^fNbG<{z@%cOYsrFiP!FI|1wWVKF_$>|{T% zD^1n}Y)>1??0>K{f)^H82}tKCD`JBmAJ?j#uvL^;1@JE6blH=00o%+EZSYeg#xRaQ z7acG?8=!UB@t3Fn?pLS*uJcIWljA13PM;NaM;i6~@8xM2PrvpS=`V%j!nkT{#+Ms5hwK^ee+z^~irsk^B+uaWVPdtXQjnI@x;?@DM;L3) zPcDPS7-7l%Kd0;4Y34BVL^+mk005mj)!RIZ)T$WY=_Lw39p1Bt_W z5QOyd@&dSMACO?x`~et%F#`TTU_q$(o(D)0W)ZKfFlzlPY>u6p3J$pXP=U!^8d}<^ zsVTe8|8O2PFG1v*@fXgc8`Spp0V3P>u`^#~)zqGmXUc3WjSkm-eLDO7d8La!^wR3b z$)1ze1py(Y9e6p6V1~qvE`jtOnhmMFIp4qh>AbUmLBrwhJnUA#jE|+IWkjO14o7E( zaw2SC`l6Uv&25)#KH`;O=Y|6ybpSq3nUv1}Xbak^=YoH&n9rBMHqEZH4idToHI^0@ zvP*^ayL+eT0Df|`+8Z+mURMR6WEK?_1^Njhhuag%QV_9D>tsJ6mv4%-WEO-V~k{eauJ16F`05VkX)>i&n%WzTX3=I8pejDWZ z=kFT%FnIlZ##6f@EpuubK(_ij#Mj$^07}t>tuHPaXO^{pK0pg>QAq8(T;{IZ=mtVE zZ9n&?rH-=26{W!Fc*_cNVhqw~JQj!C0a+zhXC}9>9ww@53fRa%{?5+#%@NQ62_`#l z^|*TCshCs!j#Z7WSjr$JH)wM&&F%C(GMA3GQ%Wa{flCH(?-E=`1?5e*mPX%BgZGf{ zxd(FEKndq-k4fY7dav%tE@ocP1%TOo7K)nz`m7*x>ti@wa=_tEYu@xe*ft+6rn4c3 zAR{jC$*^O{o|$A;4tNm4QwC|T=%QT9tVSyDrpzyv4UxpLHPYx9rr46ZZvaY*kED*K zQr~m0vJj&}gNK;25ctfSKsAEKwd)359T55Pu)TD0ih=3-Toz%3POLf`YDN3pr3Ta| zNO#WN5e{T1y)E)R2&*>ZxF^%8P)fv+Sq=ayea3}ZO`lnl^DrElsj1TQ<37t08mJ%m zkLd?QmBVoyyL*tEtzZa;US|8_>hClh9ds_(uK_ADo~bmzLXX%q#!A$*?*i$G=-60$ zi|c-x^EmDoi`KKC)Ken!d8G@{9XHQ1PABnLKRy{{QvuvtNUtUP4QH~pEg(3O#ni~x ze8GGE;9ru5pfm@_E>#G}f7;oUcT`tPpjUMy z>yX?7nRmB(@)X&bEL;!&%V*y3GVX0HGgcetQ@r+?I#()}9v^g#z6~U$1QM%H$3@!3 zp~3kQt$Y%oh~3goW91Lda3_y_W;|=S(P82|v`oa!u?0wpG~tb}%@BCRHrcE9Q)CV} zA4-#^3~neM0gGLa=`-jZLGbU}-Vfk6Ml!6m$304kR~`a|2OYjKZ0t3^iEh=nLxf;E^dT& z)a%y_<-NXT@5wv?&t!$us9l@+DU$7@#nI02_Hjmn70LIn%^p8er7eZ;3Z)CMnO#>D zdakY=EQL_i(^We6QSJ#Dr=@jD6q((bM^T?-*+~*$rKN)PosyYjom!Kqwx+{*7dIaN!ugG|)bFBow@Ra-b^KjicyI5oP2PITIzB@DlMo5!%ycUntxBG5Qfzi($4Fv%Wxa#0 z7RbLe93IBvh#k`Qdfw6%oUP*_k>sP6fS4vux-e{busoujC3-w2*ue=bCj_4AyZgXLaUBNs z!}S{C?YjUEkImj(9nlCL0^Cb7LWFFCttoyx=Z-+K|FZ_Sr0@31yGJB9r-K@fN}o4a zbc_Q+t(O}hG5DPAZs6RoR~WdAp(l`+lPgh4@!bP=0T3Na9ms%!9$F1a8*i`kH9g{> zjZ*zMGh?%(t0Hk_YxmYaFY4DD8)aJ2nJPHCb89WlQ*nFU^{@zb%Xcs0yl{>Sg5lN9 zQT&Z$3`9%t>fj;pP$qTjYxk$y_h-Lm?g;H|T8A_!)h5TqwKTV_A8oIi>%msQb|>Mp z`O|E3F4@P|wBThx^Hcg7pDm~Es`=(fMj(~3rse-r%6HqPJH9vBkNA1Zvmm0IFP%%( z;Uv_G7}sU6YqnM+#o0`vADtTXb^&=rJw?^jvmh1NCPeThc5CFvv#|@EmPz+k$CU`O z^gVsHWujw#&pkN$A;bz=HZYrcJDGuzv2%aNH6VsaQw>?GZvrx;z>~R597Q^~c?5RY z>5f;ot`!c(lc=WRuv@sgmJGq+gNn9VslIuK+9CK>iH%iiNQ*2d_T}QL=We8HFI|1V zfuB{xta&H9$h;Lp(9-?PeMmsT5^y!sEZg`Zek_lP$|E0MVpX+UC#NFH0KbiJJMDG* z?E~QGs+@rzsiT2C1*NS;fF@zi(bssEpPzqutnStnmpAx0e1Wt!TZ|(UbQElwHyw?3D0?a7Rnt>_&-xHk#3WaxKFVC_%8A`$k4?{4r^u z*VI89%r(mCL?<5=iLi%2aPzCG!he~9f1AAb2J}SUBTRDs*h`D@kWnoWpldz7o^x zfkIG7aB#5t#zb=@oi8#RdQAaEI&t-lSk_RE;(sp(C-5Ul855-oD3lam2F`+|ALd6g z7i*v#kp+(R*-S-C1>qbg4w;iTco2abiU2%ww8pcutnfwJmZP+Mayz zHct^g5L^QC3nBner0)(+>n*YU2!k{xLiu#AU3MQJ*3s zA}m!fA3l6IJlJa|nL&4<6MH*c+EMN3cF@NqWoSOje}jthgbvXX5)J|rta!!L#l^+K z;=@>A9siSSW$EWq&t;t}0Ja8qB!x`P@#Ow^#rRv{ctZe>91-j*CqJW2+e zOz-#2l@JcD>%WH@MSwrVNJ#htSSC4n`D6*7zK5uU5j{x!`{$#$izH~hx;|+~e26~6 zjpX04MpO{@oF-(`8lflk)*#0JS59mZkd>pBz@~JRM>~ceF!>b-8Z*HbDPa40o^{_N3$}og5rWI9e6h zn_!P8c7J|7@ETb7bE;Y_|MvOU8nDR%hDAn9?(K>52X?yC12g2F#qr|zN;$p|&;&*+ zg+5LHWZ^=AiFLqL=CIjxuen+e#jlhJ^buT66n z?VlaYk^xPUYgwND)9?io1Mc@SbIrf(WDgUZQZ*#!>R(1x1|P^N4e{mtWrOD^DG9S= zv_Q7;qzy);fM+LLJlp%1eg8kh{l6IQ73b0%NHB!yBN{NBY}v;5iHtfnjC!%3)jQJoVj~L0TB&cm#XaZ_Y_$N?dNS5K29f@%w|tY z$I{0<5azyXk`%W^I1sQKf);}AAIa%>0tGyg5IWI|p!L>HQz!G5d6)1d=mJCjUs#1xidIx8S5~gG95w1X!s?aCsI+PWu zoRJl_inIX}z`?CP5RGjj)$>~UDj&AG%e!h=5Q9=Q_!<`}j_fqu>|y+qYH!qb}E z`RKN?@Axw4sJ}`n{s_n2aU|dTezx|DpG7|qy%WNbqqLuwHAWl4K`HwQEk{ZZp+G|X zlz>+D8qGrrQZ7sM6758r!~NrmHwV_mvo2rJV}XHyOsyn2@#W7kf?A4@*Fs?!y&rRU zY=hv}eBD`E;kwpCm>n{^xZCG3GhQzsMkCB35(X_|3OnWV33K8(^FMu5p{s7MfVG9@ zT?CFNiHj^tdDcw+24nBjvUS+yDwkSIMS#JED`zwKP{k=ne zNMOhg`j?!U)mgE(Rx|Q#^FqvgcV>QSKD@1BkLKZYy{P(TRhV-ghaYDk-pnzUVOaG= zQJsz*dooqFz_q=5)f)%zA^d#Geq)Rs|GjCZ(Y47}bp#u!0E-5d$ScRVMEN`U2jj}M zMS;$>Fb{g!SL|x+>=0Ndcw4Kpi#Gvo2sx{FGVCXA5D(Gd>uch>f-l@~oxFDL52@{Y zhe;6TrO@`};gpFu)Gm!1g8t91Q%>{VLMiDZ9_sVjb|T*dkf0-x349QT+SBL$s*7;l zge46(*+n1^HQ32iiB|@l+NoNJc?fwg0L}4A)}MKetnpQOV2k~=^XJ4Y_*Q5igwHYP zc!FZDFqnJs9E6w9-@o<`A{!Tn`W9tO#u@e|mV7eSL;P(ult!qvKuN5RIHn^MXLK!j zLu{xx?m66TkRcWJw1|v6zBA0Au!cESN>G%L)0l)gBNwsGXVol~UE5kBo&a6pid@27 zsajmTn5>5>?6S)T&V`26gJp-Xm&NsAc%~VVDkFBc18%s8OA9 z4n(|&nc)mSX4gNRc|KtIV*7<%ujwJOd}i#5U6WcvpFrRY8VGtV$Z7Cton)PbTPi)s zO_)p}(_HE@#EdaLNh(cLTY8DVjGTzF9+Mc;OM>DX=|JpnxrlcT62W<|Cp0H)Cm1KN z6)E=uQ<_tfQ&KgPG(%FWk~8uu87*c*CTWL zc2Bi{r4_;VF8rHRvQTl>l%lNCvBFA8X{lDxeNlhumnpMSwW1RhzQQN@M{%WAvnUnvalx-#!4(GEzrz~S$6U0@d!}be|(6PjP#78#j9W$ zNSR6bFnpHMm$H=t&%$8gY86{&T|1|#4NBIG*Obv@uk)8O)b?bXc23WX%LeW8x>*@ zv5U~nU%_ZY^^sl`&CJcL=ur0SctUxU>yZwr3PB8^!&CiqI9PcN*Msu;6%1THoD1A6 z3h!H{S3)QkuM+Xu-mI{9M-qJ9haTN%$4>YvEfv zR$JC%Rz_At_SmE{R!)lvn-V)b;}omr!OJ3x#J9^II;D;42M?3xkwb_A`+k}!f0SkP z9Euz!AKKd18d@B>|6on`jc!|WN@J-ZPo0UbhE7eRUj0fvStGJO#a6}0r9Z`bxE4pV zqKen*aplL%`)ShvnZRKuEoiMMn<|^gBc4wwI$6sUEi^4wSHfNeJ{ga2_rFk!0t*Aj zvM1KRk?-$rQ8_NH%^eHdpF6z%q{6xET)HqkckueR~Wy3pLvPNUp@I=SW@ z^HJCgx(vP6*0wy|I$fO=)s^IxgEl)Rmt=>bz9A=Gm`wdl4c<^6-aF4*oV(-e$H~!o zhEco3)eD9CT$tSYNY6vN^-=KZ#wtc9!p~Q81Y;QHL_bl=ki4;s-}a&{5M6#X867k^ zGHpY0MM;1oe&suQWiqNyjMgRCEhH(lAm}0(Co~((i%^5`3$YaO0P*@&CXO%nXLJng zD(;Q<{XspO%1)kSZRBRc84)?6??gdgJh6_@=^|~3BBP=R+8i(2-WbXmo$Q%EQW?;a1>5<$*q+LqB*;qu%c-;&6xa ziIdi;IVhmp(V!uWPLt9z9Y3~(`r1d=dL^p$>0#=j8LIB{(C7D6Q5F! zqQ1SB=%-kfD5V68=jJV%gjuJRRmtP}PBjc3{pK4W#(~9B!SP8-W4i6cI-=g71}PC1 z(5Kv$ag^O8THLU#)F$k0Pwq(jaRjm&{gO5|of%5dNT}H!-A^Or_1YmAOl#%r7;M`KpUaPt;^|Y@<{WeWJ1Lclu>!- z-6VEf!1|r_oRtuJ96QnqG()d-?~!i53?5t?YzF^0zyX}~OOAO=Uq;yK00I=(kp4ew_#X(G6{7T>mLb9FDz**Ep>Z)qNy1; z^V6!?HG0{{L+)qB56ztgI+r^a6R$4a+ZEh3(PNArGzi=KF~%cu`LlAK)6=Fe3dMs5A?`dj3Fr@1dN zWp5o1)W*%q_GMarB6PW1O0Hz49IYN9`n2PBeCn}!A%mNQi%0LNQ)UOY=DA7Tv4dZ1 zUD#*v(6ekkbZTnyQNACi4N#NSMrrcg#@kuwJ7An$w4+#Uba6Tyj?#M28nxS5_4O&- zm0ew)zec#0Jh574@Y8?D+IPEOJ>Y8PisF9dF?UCGHon~7+Ru>rGsF8h?fLsv?L+c> z&Hj$Z+x}S>d%A6(-nSjkMj$0Gn2QPcg@ek8x0|LZVy{$ zYEpLECxLFTCwD7}z8@bdFUT)8=jDnsGr%%W(s$ZteNR#svqcrxcgT15@3g7i)|4Ou zp@&D{)`n3b%=aM13|2+o<{rYr!zf%;KqEX*mL1~4_aggi-aHbmqIB5WLRGrqTX&Fj zygyXgG>4$Lg_v6QF)tEMI65x)ozu-5e45qqOm{dhP78TcUDnZ2QPT1JOZCl%L&E4U z@jOL>VU=cc1Q+j0Nsu4#6|M?bw{A8GsmM+t_*R}_uhnCTMt{zi0O^OdiMoWTj0^;j zkU$_nz(C?bzyiNP0v}#T{C|HJg`|Rj{;M5GSwbx!VE%KBEb#sEF9!I$^!fW6DhukL zD`04{p#S+DO7rDX$j=L-z!##uxP}u11kSsc52S=5#VG^?KZJy^fU-N};WB(0rpZ(P znM^fdLX>kQ)C34iK^Y5ML}WCgEx@c#iTfZN7W-h;HOmb5KAkKq0tE&e1`8GgTR}PH zvfd}|nf?AlPmFPmfkZMzW~=*=OoqpH@{Y%-_7H>I+r-hGUu(e-PzWUc|NQ9H?IgTc zvWJ5~=ZAp&=SL?4|4o+siqj*QSN;eSdlJC+9b=M5q7 zHNyXCC5bFxg2U%+mOqUEmWt>9r%UzInYU{wnVC{L>7&oxWiT>sxn|wq0hp^}Wa{G< z?2@h9WP!{l*TcC6yH$ZeIPl>-^H!=fv=LpWnNC?Sbq5ZV_sld@q!2nkNvr>_or~bn z*v_`N_cC}qo|jX_N|@^KvXT50P>*+Dd`83G&H!lg0cj!_D43fvWQcW;5$sKw(_wzL zAp%9Lzf(`NK~G2|;V0Gak@&0}7qtP_ zMiFBUG9V%%0-IKKDE*V$`R{?vACX!$rje-IHMa0D0}&8V0#LHA_+~&_?Cz)flY0(3 zAhoY=e0fR5#)8Sa1fY;aMENccha31xxb_OW8df|@0h3Wnh*ca){2yzM%Uh0{pY9G2 zInz8rMTvU+*+1K^SG*==*ey~=xsDNG(2MH)F?*tg(0G)q#0sNY_^yAa--CCR59%FA zpw6(8nBBTO5|>)_*9{-lJO;m0pK-J9NGb-Dj5f=r>$MVDbd~Hf5RHQ9gXMuF`GH-{k3Z0}! z0f)=;HIyBC7 zr!X49jY3N_|H2JLQuCQ%wQY6WlH5+|hfGuI6$h?LWwk1(tGp@u68o`_ET#ksb^md! z1~$Kp)is<)5o{|J=eh?sV^j7cMT<*DQ-+xw_al4A%-p-HFoMW5RID+SvPE_5&6<$_ zwNM(;HDgHlU}Demro2mUG)gZ8b{H~@j}28#`Q!s~8MUvA?#l2#l7KX2jFc4lql*<< zfG0EXMJg9PEZsk_I~q7!CF!2^`iGiQ0|>ohol8Hq6QV}na!S2MOu`o$#gWe8Qc{5t zL`qsr8f}i@pdj<-cY#7pxzD!Mu9($oKDL6>Q*o;km&1XSdH!kJSh09CGq-D8%nbgT z>VI2C%fMiEsvnnd0CfDUp-VQ%1Qc^~m=_{Gnk9uzWe>fjj;K)cCPr5%_Y#BozJxK6F_C zGSg7+{|6x>0EATN&7=Agkr5~XESC-S`Jc`r0CZCsDq{WVDuOUc9uNw9c>nAimIL?{ zEJkXiKY_U$fb^hA1L8kB3o8KTL?mJTkAGVSd`gc>2jSKKbjE^Z%2me^ zc_+E{C3)4C(a3i4J<2GdW9xpU`5CdD+)eM+V59f$TTagT@Dl7D5r}{(O!WraWxuBj z;yPM(_Uj1R^03wbd^YRG=coI4znjzbNIcevht3#IJpdu%Y1N3_*8%{%MNTF??~TM~ zj{*SoR^>a=D~~m!XdYE%@n&GCmb>>fE)b#VS-e*h-In z`xdH=}^a}`uwEP0|z!7{~cX8^nr$ErDc=I>u`n( zXRDAuIm44@3TfxznHE-DsQ?i;$r2}#g&Cgv-=ydnXEzQiI#$dweaKqrr4l~OyUgmu z-xg;2E?8Bxk#UzaTLYl1TdCg#w6l9&?(UxpCp`f@j@$2#2fa_)uA|;ClKocL{+8~! zUtH04rTO#KURu)BwG)>f$3hf86zm)Cc>V_pUa$u4ihZ|*S{u*$CWS4uiWo!yA+G$5 z0paB!ZH4-x_1{qddNxF01zC_}f%PNWL6iQ-Vu?fG9qfRz-9F$3MsVaqd&)Fw%DAH5 zGMi2nDI_N&-kki*lZcD>@&%vW)>HFe$?=-nhw@#}= z$yd>%+p?dzhlw`Q>Y#-3PCc|tt7H5CtZ6q7^m4{=-^)*wz8H=vlu2dtIUk^07oee` z={BppN{Elg#=+6F8y5BOXnG$BL$Pnu;(Em8ygLR=zNYhD#7TMMV!qB3yubA;QB4hn zR;No*LLwZ@HuU2Td=N<3{o4!|c&ahZz-}IJ7+CQ>=<&HrQyeRs)m@IV8apR>^f`-1 zoQ@=%mA-uH*LCjAlB&nTnNMYRSvWM3Yu65Cw|0^L!h_0p{Vlc7=d6yWM<}|jsAkE1 zk=$ygyyNCP_UMYYL}F10ua5Vo2s30jxux@W>vfme_nitAEAUm6SH{$pRE2CUZZ{hg zLSCLfSpe{aHDy}XJ|mBC?hs6h(vl#WpMQPJJi@X0)pR(e*e=g*wY_20_a5-m(C2Bc z3l_^QZfePyeoswE%4(wNTrN?3PdDrWZa42|(RsBTl%g%8;X=4v@ zJ*c0Cq~EG-!xMYUi!9sspjge6HSOknS*W$hoj>BT@AQXs+8#pWuv1>4*>BuVvD%+5 zovpQ)OrX=U6smhX>nDarQX_GA1kaY*dU$xGlrcnnI&LHvv?4Y!n3@@Ol>A5EGEL&ITw%HEW z&M(l@(J9G=NwivBDmAR!HpXk51$M(ddhJ*F>DZ6x$35*##IFuNH#Ih1Mn22TPy?<& zi}%j?$B@Pa@AFOYh}X$k{{;4+(b2?JM7&j*;ma0M#=bf2L3_SCV5qbG8ABRD8^yI> zgvNWb)`zP%RU&_PZes_&Iop)uJ`<)D84uQt=4(H?=s!&1^J^D%;yM3GzUp(G9`7Nz zC8gAPwsQuww z$_LaSTXliIAH%tu70430LL<53S#i#4GKgixSPncm4b+mtc#z_TQzU`c{ZWI7`2;S# zcF~~U<+#{RhF5uf+{@u135MN$w7=_nJdIE;rxIM&9hcgArJz7u91RWudf+BPegDWE;ps3%UE;)@Bi?ke8 zf4TMax!WsDg=@bU`E->)uT?+PO9TrGdncl&*HtK+z5s+En9XxeDKvyRPrn#C zsN6q>zrT|InT;;eC4GN&6#q0PgceLZnD5SC)jw~R`S^Ifdhn^Onb7$=6toCO!JPTy z^n(|sSJ8#p(Lx=b3K+O1d8?+p^<2^k2RuzB%Ul)eX^}v$Q$n-sw(7G`8n*j8P7bx` zD->sbnteAS8s9_ZT`<(uIS$~<#NR=B@)7v3f{80J*zf(0td>jWtr(V8^BkaUP8N+$ zo?A{rdZL>67d@By*Ea7%?SwQmZch&$t(2~~vKI56dDhQvj7EInPo7s6pO44yvB2)! z7aNPA#0!(UKW*4`J%90qrhIP^(*^HMRharbKBn0{bgb{~k=-RGCg%F*iCnx-F)-wd zu>nkxfJCumstO2U^WY|`Q^?JemclklbD62PRX=EH_uV^Ls4G1`Y}(1#$K0I(rM9(n zmghkfn8`hh#k6;2Iu25sp2icsK9BZ$5DMRK93$r)vh2{LP|Kf>Mwuhxbo>o!Sh0M4 z(zHV)BQs7?K40_L)}`QG%t<5T!+`Wrx(Yb+m2gP|0^{F3Em)=`wM|pn@3^8kx8KCc zT&N6PCeyN}9mkw)|?#R>7aPU}+g`;6OO->WXwh3!RDkHJsk10yY-b@h^W7fh#UXJ_u$fgok^?u`|=&w4QzRB?mCd89pM zWo2D1-d^@C_Y%D?WR;YBr*2}I5<0J2YaSc_#<55E!wkJrb(`l*dHVcEr4?+QUBs_Q zY|;uT2RRuyZCGAZThnTK(|W;Fs)q*E8f~I@ekO`Tdf-!SmuZzNEWnBvjL5p~8CSb% zc=t>CvH6hjCU7x`Mq2^Z@=$U>q)7pL)nd$%;L=OaQU*Ejm_{I zMrx4eu_0f?%kyQ2Ja8b!Th2?gXn!w}it;#N=UYFUhv4(9nzOAAj}G1)CnxD^WrZ#+ z)4{7Ysb?koCCTzg=ZWuo8qX!i;<3yni4W4OtL{K)vcV>Dtl$~fG*oAtyBOuG(81O7 zuEO&LaDmwOX8q18qtqNVncXgfZOTenX9>J$*O8pIZI{XGLsk*~$p6<(n7yp#{FWi=UNu+}~vQ=8EJaZ82ioK?;jRYYcilqX9#sh@_>2~fyM?ZoHkuV0pa z3LqiuFI|QPOPcv(`HIOKd0Y;Dr}io|=xFHhg={CN!!N*_YlH)_2>$|-dg+1d{uDKY zfZjZ7p*Ch&$K#b&O>5bJzK==6`GEt@OuY~JH;lZi2ZMU6SucH`lT0=n&7a+@5Lo+_@nlVlikgf>$8PoFrF180A<=z9mzd@rDf`t@E=WS{Fnf>eirZ?| zM*aNWu@7H0%y9MB%K^$NgiP5Z5F_&V{d*t|n!o*K1DQ^sRG3aD|5Qj0n&oZnpc20< zoi%pMMofbzcPX5|g=Q;G-UKG+`9-O3MO8`R&p~S|`$V?O&ip2)9ZR8$pVt-M7RmZ9 zCc6Pdnw3)Nq$x?|;k5gR3M@G;RV>6jYlxNmGT%ywWDV0l|K&kIFqRpOX{rRWR@V1C zrB7N`@2YwduWmdOYzvz^~^qJ9Xka^ulcM z*)J|kLE^p`VuoZ@xQ@bkU=81&9}D4f#nRl(5BoK!-C(N&@-|s1SFaMpzdx*rezKft z`HVuF%69oiSY~i`-SqauJ^f#MnFZrzmjv6&cdTVTH_EF_1wNN0?ukS zJa_SGcU8>pnf-}eu8qhj)3>Mn%L_2Q4NxgnO_rF(6EGf*MKDhX!|*VR2-~qrOL^$} z-kQ+Km$Evs4vP20mF=NJhJIQHc`${C^D9r++bot;rI?q4%cKvpk7F5)eyFQVPm0un zbO=AWDk~Rct6FYKP^RYzqlhp^n@o*;h&uv2^0aO+W1H8ts_z9!H~XqLv-|f%6<&NG zrRBFoM)&1-yxZR+s3~Z3+{o%t8ShX9r8E9|{*?Ai9W=V?b{;O4w2&Immz+#=F>~ZP zKUufjs<9co+RA)d6j%ou+j>MRETw{_$UPz8-2+it!-upJf8GhvbzYHpPo)@;%7&XV zd4>xKqbRN49fVjQGIIg`j9*j^fvH)PDGXhH>5!Kjf`VU=W=a+WM`AJ3Y^s`M1PVtt zC0z86PV{fsMyDNLK;W2>L@UyD*@K+FRMo-3Gd10=+6BF}D?kCeYx~}AMU<7PxWyNV zd2nCN8NE-%D{b2zCyP+TQd8#yLkJ%!D(6nH@GK*Q35F&)XwwKVFdMbIUTn)CK8OIB z@HeT>d<|#R;;LIa#XiyJ@4UKQPzUVR6XXbWg2&}Yn#WaI_*M#KYiS0G2@h4?D-W5f zAl=yd#m8YY zoxxRnsYLqez*`n2!O5Ds!8mLejt51{&ZLC$iQkSOr~ITmlV1tbD(XTdra;tA4c0^! zb~?UZuwnqa!2~2l7s=p@#Fl=~3ClnuBS`WCDjR;W_a+Z3`cIx<4dF#Ai%fTHgqsyW3L49+bhE4Q0dzm zIZ26CsYghnys%f;#p48;u_I8}*iSFCnR7e5`6sA4bbI-m@mdQdR{g%g={j?`P?9w& zNGJSMtpx?_Tqap`fT`1}>PC#uMnyRM+8HtU(lwp&w>r#(e5b&=Sbr3u3uJrxwGo~! zTuPcD)HWMigL4FHONP5eKhyoN;IQGW|#!Q#+Hl#GdfIQ5XIUDTs&Sai` z|H>1}>qdYaVd>m<>8F=Mg!S2NTViXv^lRmN|`9&2AN}5tG&Gld@w7gw*WBL)(@l{COO=U>Ly(jcK ztXjC)uSvW<7^kM8%R`6Rp#Erf{23aJQ_0Nxdf5#gmEg^&l2bXw>&m}K;Wi+pf2S~9 zOamqQLRa6Pbad3;mTe{gJCyJ*QnIAH!b&IFqqP_i5qHIeCmah1Di0AU@xjG^-BAy5HjgxranZK98^?XU;`XX?FHlNW})@2<9^xKfZD&7 zKdjR4g5o{+fC;!T{m>T@+@R!XM$2wbNJejyC2Z^XK66_j=##U^0D&Lma!#{?OH0a} z5WD@uLFd~zg{=J=+b$>+!?&!K-#tESF|h@46^8rUDl8W@>&P5-ndzixyQPHlPr;_m z-5cO>TF+Hh8TDIgSsA2zEObJKULUX230;`VsAJLYvqQqp2=bGLX3!Pwc$r5oC+el< zHEq)fZ=(PB`c~=71P4VrZ2b$@&29WM1`-Kr{sJ1f58m`iWHbdFe@hL&$cR{ zb67Bq$sefd&yN7if=)&8KMe2xnfRY&`@dEE57YdAd-3m(Xr{tNCI}9_nRr_CUN>Jok`|0>h6P}byK6Esxqs(nq zHOl;T{EL^E5Cy>7hMpDWC4^O!U0YW(Ju*i(ghC^v`dT{5O|m6!jt-2v<_y%S6zY-g z|E2Arf5<|ODIG0BA|iVwco-0hbU0mytwY>ZnfG#Bt8f6Aulu<5S0LXjL?Vs;8r?l< zVGN8BUE0n0lH8I&0!1{)l3yhR9UL4aBqTI6G(<##XAdF=IH_vi2jKS;TbR6c=?`)DgksFLH*- zXjwU!J(b;#{=)}~-(jq*f7>EVWj~(PMm{7_uZghRapSoMFZMXAmo4t=>$}|SqC>An z7qNj3_)VUGxe(&JrTJQmINm@-v?!dvoD+ri+Q#OiLT{9-f6;-_Dyj^o?k!8Pe>-%N z9w{;s5>{xQM5zVfEQ2t@3~s6ndf5c%Z!s90xz;JNBtq*D)n2(4p&Xa506Vk%y)4AFUa z0^%PAh76d($vWs!m`!AO71d0j3>V;zB+UFU<}|!_7q-*4sr^^bk-&e0ir&oofhyE` z7dfzp5c`XqZ#!FWe{v1U z8cV*^3FZpj3Q>9b-*3RJt+ zvmYfYrJoO!0Ero3ING!tY{C1}H76>V?RWdM`3S15S2}zTr#*n$@$mAw*^CC{F|Jl1 zY!?c7^&;RN&DZkyKHTI>C9>Hr-PBIXPA=YF?D$>Hn=P166^-u|W)j6{*!}wTE7Rvn z<09}_!z>+JK7Z{YuUC+tkc8%H50OtjcoJ-&z+n?ufg;;NHU$qviogveOBTb~us$1v90 z25LGyKVBak6&Ge$1{2?vI{_+Gsf#L`%?I7qo7`w#&xf=AQJ{hbDA@V6d8Y1_>2=(c z%xqR~x4IHUOs84vezP80*V@Q5RiXEsWE559n1T{-O~0!kMhL#{c;?Y(u)}-@Jjk5i zoD9J3L{|i0hypnrp;xHW*(Zs)4Jgz>MdaHUKR+6+5<-pmQkFYgrjffQV2AzsVUzfo zii(QkxN)%ai0lLMr>8UwV0R-hpbs|@uaEZX z!k||oKU_}AIqi(pDPAtC3AvRu?9~8P0p(uf{!|C7z&DVn9X&stJinS{oz=!P{m>7{ zqPFd~rd!H$S-g=za$w#We@8{+xtm?AR0vSevMoeDm*YScRZtfnpd`u=e}T%-vTf=c zEVe4FNo^>R1B%8#I@T1bz!vSjb8>w-ZR)@h%>`k>Y>=L=K0nQ-{qUxonVlcydv5)K z2_(MFKL%20EVl;ZS>c#PL_~n<0EKNlxe^IngZ7!!o;rV13k!=#5m>XL^gtC^SJx}* z!ZgPfj~!|e8;y4FraD@EA!Z)wK^gYtJZj>rHm{?)*?2Ww3q_m{>iVnLtv-u34Hj4l z6RVyFAfWtV)Yu&$Id_JVb8S#2k%+!yYa>us;e3?R9|vPtnGK2o&cu?G|3(3s1yw-b z3>FK&^?Z<)Mua&Dd^C?`=7Z57IXn8`cfYvGLSQ~$ZE`oW*y3h4TxC015NS&NaCcCl z%2tz2VgQs43}!VU0Ti&@V!aI-&*j+j!wz4Ul$LDL$85jnCm^8?MN&3>obI*F9L1H- zdT&@p zv?n9;2nJ~k9Lw+h0?i1@l$}ceY>JDK0FucYkngMXwY)gl34X#pM5O|0kBeb;RcUSO>BHc-Vt{dJuG{eim8?h&BZm9S>9*eyyv&vrb4 zr(D^|qDrS=DJ zy~%(Y}MH9eoYk)q_+FWVwsj2h3xc%*z?JyFLs*=52GIDbI(f+<%} z2&sP~f9QcH?b4Ol)^!S%S>ROvdENf=0jl={ggD28uK{4vX_2 zEyY)C$hVb!rsHyGwVmZif2o({k3br`kIlhNFy&O|e|&|6s4M`owo34r<^mP8;Vqe9gJ<>pif6@9--}?643%9m z)6vpOjgPE#1pEa6;LWEp{wWY=c- zwH+z-J*)Xxr!dK#!3yusLCZ>CtA_eI1D@PdfcOGb#4!0?Lzk~f!XO$ewxM2!c3`{iocUTe$c&TLXn3tWSe5laS(t6A-R(@Y{2-OsOqo-I}qBNX>p86R+SMwkHMBAkv zvF}yer{eQ$$s+S6$YEU?w|@w~_BDT%)D)!|EX{2B`xFRcij+jGqD0i;pS{r{n>K>^ zCy;jGuIep*8DHkL`709y1Odg36kjti3k&`0n#+?Lt;NR9W$?7~_E0=8gZ73$dVy}Y zAd|tWkRea|q24}-Y&D&LaIE)Pr4bHqk9v6~qIS?!#=5ef;)>~L5fZxUu%*_fpv+vz zsgZS+T))Btf6NForVs}e^4;OCmP{b^hYwS8m4FOLwx$mHoF5wC4TMXcQR@KV40Q$9 zF>Pr0QhGAMx+Ub~$U^*Ms8eA8kn0WX%id^zR^--`HXBkAfBQO4v~y?LM%!0=dho4; zk_`LtQ_yU+Zfy_b0EL>?C8LVsU2o zgUJ{(*HC8(y-xEksGU7^PROX}5R*+h#|5WUNQhR$!mv5MJed@aHBZSLsyVsD_Nc_R zXUh?gyL_K!6;f(J4OeOV^YYhWqKxyHM~ih%m5K6SY=&oXQ^;c0PJ6zaf>PeC_~XpW z6|zQwj;VRx={SwCI_VHKU$Y=4AxzomXoQ+ zoOjumov&#b^>b%XA-4|4w0O7`ZcXxcX>a&NgdOGY0^o_>098kDE^cqUL6X)ebM+E!0KEn3jMHTS(cFP%($uO;^;5u) zPcK!Jk_v92IOf$S6GF!qL+Zk*Ve9=if0tT`k(N z@0s2tWQ3Xd-pt-r8v4b%^nBc*RoO*s-2 zDU>#IC&AVNFM6`LC8X_kUb{D-X#AsW736lNuv!f%*5HAR{c4+*H|C6@S?xf@7&E#& zu|_}$HF+@(wnw`V(U5~++1h1yXRegwYqJvh;L|qKip+B<Krf)CQOso46vq0$UCf0`HG2rQZu$)0_i7;@{&zAx zvJI{N8$efpLX5m+)H`M!*#Sc62Jol0_FXF;D&|C;RiEKCG9GbZGiCTNROOsoPY~w} zo^ih2B8_VJ_S?@~n{!o0;ERr8MVGjdz{zRET)TA|J@P@8ji28OEQ>bt^*VeDYS%jI zT;&zIToCA2^VFe>4nvEejUR5#QtAxP%}EmK*s9*g9u!j}O_XVX2aQ%|w0QQ%zosiK znf%B|b}Z|N;3`{pf+Sxm<2LQ7t2=o|lHPzokq*J`G@-*p^Oe6FRy)P`El4uyY;2q+ z($SFAe)EPLWGI%tM!YxjYXIubv7%FGJ@kj61zT8#QWX#^0UG^2q{r>;EhMX2V~z)| z=Ta!s?ysup+bk6mFoYxa1R(Gophu!kkkF&>E1DivjW3|a znNfTGi5zYw8(BYbp@`+Rn}1=so5ICk8>Gfvn$CPz?#`C%M2fBy@C}1~y4m|8Pzr>$ z!w--sqL8ZXl0QmC((7}9ey^k)!_8Py8rQ$*hNF?0@k?6G+Bx zM;Vi)(ItBA))wO}C&DXHhsQjQ_k_o^^edOYF=L6bnapM~j-1dAGS@F`eD^r9-!RF6 zFfXFG-q#$JbF~_elucBa!6z!YT&)rE+Vz!m09CQ{>DNcDpLjApUoZ^XH~sRxt8(9` zTs&HcWiKBp`Iv2LzWk5w9MR$$K(KN49!!kCra1x(%{m>O*HJ6JjoSJWXN(WCU zHI;c{`^F9->qjBIMrI~%n}wiPd`#Dtb|Xa2&Y}t5P>)WQYb`D&jeaFQm@+k1S66en zP1m>hK57OLm#3wD{u1J`Q14`7riQays@~-I3%?@%`Midj4+xQv=?9J%K!&%~D7j*= zC&T+w`6GK~&B#cV?oFtZ)nyN{r{5b7%}YGZ4C{h2Pbr%=)=TY2)mceXTJTonaB}1+) z3pG_QJBUNTe+7+F8}=`{QCN@UdDrMDc>D5D=kkfdOVYdk3*UXM_ZeCGuNGSy=h39e z!iqM2l-`Ah4W}!td)M`wXKcAtOSu8-#qzZiwf)p)Plf%5qs!sVjD{0XpX>4k0~4gb zrsm~O|udUbo)1lh0XD(}uVBmHP__`(+WKQ!b{%Lm)j zLnIlrE|q48yo&LKSDONBf+eV*eFlDr5tsNK@x}K~*4red#U3ga1{I1(ZC#e@);*QS zZpTbCt^P*g&`f``zx0#XxjTVfk9`kCbY-htjnBJk9{!3co#pW?{3dA+Sn=ZNmtTgnwbfsv5?=&xS1*Rz*b zathgYUf$i=pvXdeWr`rbw^Y#<9PAV*z)T(PZj&TCF8_#^r zL0eg@@2NL2{Yupp!8kOMGjOtl!Xe6((ia=6fbH9O@i?`vmuyo)ma~DzH))Nd*@iRnqFxk0bVr!|cmeJ13mOO4= zjGDl8b|IPS0D$p&$pRbOl)Ti+WK~uo+*A$MkFjl}*o?~Q7(?t>{w7-&cd{odhI*)J zDH8u6lgYS#5!7s*4AM;1+3L6vOPSYO$q}rZTGgup*R8L#)Y2z-s67+UYB8(VA74LL zOv~lZUtpK2sT_S%JYZXxq44b^TKKv03{g{RY}Kk3uLL=;Kb=t^(;!rTRTc1ZY+hbk zbGS--DVYk3^cRHn(+g_vF4r*n@n|I+qs14uvu%3Z9_qtN5s6$9!87;^c{MZ70(w5Z zWv72Cl_Yq{bOPF?~c-Af;zk(xGrJRF<|F#Au!O^tR^eZPtS(;uV|R z)jFFZ3S|cNoMKQfu4P<~6`3}rcxqSQncT8X^fY<-$i8o&970JV9EBB>|l*LP%%R44{lu*sYKd_Rf89B zav`^y`AKDC89rAXj9mLoQ6=N(xxd9Du7D}EwwqnI)T^uS$tKgTXSpG@m?ZyV=a!PP zQ9GIL5tFpL-5XzYRi(->_?&8GEYIed01&SX-Khm=n$(ZO&Q%46G* z;iP!pce!>^l3&mx+@fupSm+_eJU5g%xkLM4*Eo-1a?!qcc_J3C@QC>IkgZ!%qp>2% z7|8Qbgjb~M5ytllLGvTKvcVI}fVBs}j+mvrla4&BhI6}h18G-n#p4bKlyg1Bs(B`l zpx4+u-R-O^w=c=0p!?35%!xt(fqCVrt>Gi*rx%vr2U4ct|8e)$Z*6T`)bN4g6sRCA z5}*_)-csC4DHPY>QrxAu2TFlLp_Jn879_a4loq!TT#6^Sd%n%N=ic|c@1OAf!b5o2 z$=YkL%r)njV~n*=fjo*+xS5DGyq+d)LDYh}EY+@OX-@h4@VqTD!bAr+ML#OHFlzE$ zNgv3>l-8N+WteBlDA=Wh7#ccA6E$4-_k{Sw-5kdN3V{5vy^Rzij}}T|jkMxR_eofl z^b7DK-oK9j>jl6!ViW-Bd|Q~14@DU>tsAX_eC)r9HZO3%zKxHIOVVh><&T|G ze{3@wvKvSJfVbw<43vE)3^8zs+b(`lSIaQ1V)Wf>Mn=LS5=;Fz(`cL?nLn1edXJVq zoZgg{l0t>_?_KtFe$KYmFk(P(ah%ajgIt#YFEtp$C@NU>$1`V zjEAiwiH^t;R_)UQ9P*uBq!hd(9NgR|qnIWct>y7$thFq=@@@YPPQ~!Z)iE z<8YwXP6qQwocfR}j@#sfgkUklXI^^3(tHjjoFsrCAVA_8AUOVU9C~_}yF4M#Ui@{_ zl~ld1HX`5kUEzG$dZBZB;zN3Xeg}gW)cLlCo2~$DZ=Fk`UPr5kT9yF&35W9}^AFpC z<|MQn&|CFQBV@Ri-N`$BS>6T~`?>hBTO4xNSa&d&9r_ZRuw}H$jv8%56019*i&yLP55!x}bu4-4E zO*3myJp{EH>isbFkkQ@f8t;mPlU$y=-=i^a3ruW3$AMJq_Hji&b7rz)zeSgY&0iWq zRYfn04EsfsdW=pCrp2IgG8dLK0*B@&JDYl*zbC^^G!>~9ykJajRv~rLnf7&!v2hF* zRN-F-DC>LF#o?GHmzGLv4pSo{2|3ryXzuQZt@>^!A08xdcwTcU-0nen)IQBI}Ddjs%oLCUP5uF$l2r92HoT$TCt$|kE%psQhI zTP3%}UR8$ROV2lkY$SgZ2L46!4csI=Pxx$uP+GHj`24C%wB)t*4pU^qnBL8E7(~|- z3S{qnb&))A(`Mt!CnvC*f{U$rp%Hp2Z#xt4;?SRW^aX0#gf8Z8JLEk=mTt(o{npUq z;c&)Jgms|qEWB=AYdPgNzmc0p*-#U>5i%c7j8!3=c+@bm6ABr`)1pDYWVN?lQ0f@z zA5dk?P(E`!_cdRC6PlES4QU%QSQIy7EA|U*OujN~nB^|=?Weta)m0So2)H9^2yJ#plp>w%OF3S>*1o=*AX{`1Q z7qfR;hF221c7_0H)b&9qJt_x7E7>HA1%bpJp=`nkik1XDlFgRn_B+v1x{B$)M?7&& zK;)@jo@FV__e8L-$v=&J3FE`ao43Sgf5R9XkNn!o;ng%)x!20O=K@r%i&MHs_8aq@ zJ|k49?a7czZ7`9rf&bCCG8FRLD7zn6{J*+E$gr97?i?Vmye~93+>sYm;k*wD1q6Hr;c<66l5B839i7RYp2J&-|GA;IFs`Kzz2A_ ztpNIfG&0rwTl4-Om?PjV08>yq(--3W3uq_;jBlbZ5sBpg;(0CsOn#D3eLU!0`uGn5 zA%O%!a_%i}e(-<)a<@osW-5eB0T1y39`jQFu!sFLH7D-XgZGax<_Bce)S*vb zyu1@<|3B>FKta!pRe&SaWIdh_sB*Sv>-I9=1OWv2t?xJw{RLJfRRHSndj<@kfBTu% z+OkY{D;=Y$xio z)e~q$zHH+OC`r^+I9T+8zY>5MKA1#>5DBNdj%{3Rzy36pLs>APFu6AQ4v~i-%6y=~ z{}yvr_WU0nz!sazXn`Y+JWk%;iE>zc(;OhH(E5RAH4xY8iY(q}4}6@$GtDe(qsW`X zo+%Hb0{WeejM@6$-8~Qer4-%)oG=MGZ_LZVdZ1f6$`@!9{xDBP@#d+PrCSrT`Q^+q?hd(5VT z<5fdwj}+rkFA_ zzRPnpe@@z*m6~!c4{*>x@5r@<2EkOF;Kz1v7;PdA){}8pa_z(KF1KlI5dE{=-r4jK zX9HlG1xREDKtR7fGFy?uu6l!a#pCNWOP$HcEzUhLKhlxzJ*Hc7Mm4N^*Jh^gW%0Tv zZAS;lmyZ`rxD0|r()TLH8C50Z@HgWV#nJdKKHUw*fZNnR?`wtOS1}$y{YKI`rzryDh8xW*h5$W;&LY*I20c%k-;sVP4hWM7ArX98W~sOyLqwZy ziW$YB=Znx+PaRN{U2A;opL+<;v;SvGzvp6x3*D(Gz+RsoeEk$@b_e>Lvc13gXDaD< zvCG6(0z!?F@*tW=c#`Y{d_V4H#|uWo`_JqIYLKgx8~1=FLnsv<{OHpXC7O{wkH(=$ zV)|z!&sAd&2akcTHoeK$9&#b%VE*^hK90E$;H07*6Av?R&;A;I>!fL@x_{9 zF_5Coa9`gDftHc4YK8xedk@a)hW!)Z(Y_Se{-0gXBNl+|R)LIWdh?2FI*tVi6Wz>0_&^x#^`{5Z+_2*yMTT*Zqb1 z9k-^Obck(~DEH;@L5a?a&wA3>yRVoO!}jwteuKQzZpbN;gH$KU$hQbXaG9eF@+DviR=cKx?9YrwL9oD2nss8m?PuR=C5Z7)l3L; z#iFKr;EeO$sYQ(ePw>wd1x0@TQuW22l_*F4L47~Evw5v-F@ncWUQ~5PeFzVTHSlm_ zb9fcDw)WIx(ZHk!B&T0HaJ81OhD)n|Qd+MC8wh*cpUm%pdg1b3Hq857cxe%n96-xF zR7~-uFdA55W2iQT;ETHt^j|b6!h%gmz`UJw-5}4+7jAx=8Ap0@M1ZSA8a64P$i?cV zvN#9y;qrpd+fdq2ns^U5n8;Qn^*&I}E%kfJ53*SJZY(Cgf+XEUysq7?VGdbTI;Asf z4}dxrP;MW0Kc4LY)=H8{Y0(6R3KWNHHSZMzYEnh(_4ZrX{& z6#@UM&l}ZD8dkbqs;VDSKS0mANEU*K4`Ic{T34sLfN7KvU;lIL8~Yhoz3(sGY`zQ# zjkcV{Cu(;_l#hL3SC2rX9Nb=I32Wyo!vs#XTjg;-B4Yp2QIwReUtGWdYGEw;Nj-b3 z@Gm!|)A^{sd8iMZ^4sY{SBr3S+#h}+o>(5Tqo|Q%#CYr|a$Sk8SbIgp2Ysuas@}Z5 zfjkv@DazcNcd@vhXZ_@CA%&{)NAihr9=|QnqL#ZqRj#=QFZn1c@*E)6T%lmI91XS{ zL&d+klT$!WOJjMnG>^&?>fk`5Q-e7sf6LV`cPqGH8t-|5M5(aE-Bb~#^VDcgWPN9# zKL|0OD7GyIlna@F$f3*!M=xw>vE0I{5K2j49!z9xQ}%^%z_6;HK#NDV<>vrXP&&1V zAr-%-Pxn|}biAhhL+gg1eFk1GpI^k^DO2PSGLcedz?Lk7KT&q?a~X+@LAM4}p)`j$ zn80}xo|iYe2_HoBExBD1IpXGOoJ|slK>L?gk3W4%oEY-<|FWJYbExKTJIIEbTDsS@ zO)#Vo&M0witXdxZ27$|u0RZV~5*$Kzx6oL;y-y$ma+~S`9&Mq=vmcZ1&K06^iA5s1 zelX5nK0ekyu*@KObs{36z@~zoiv={yqrnaFj)rDIoV<%~&)4Mx}gvfz3W{n3V21eI1k z%QK4`AkT)1<|?K@x3;1u0!4tPdFZ?7T`J%)UC~HfJ$$blNQ1l^gf*ZNVl1R9}I<1*=bll5o|E6V0>swr2I>CGWrO z%JV)w9!Py!e;&t_?V&q3kZ%Sgj9wK;pG~7R12TPXszdq3ESnj>pa7?pK zk2X#>Js;#Jn>59Bd+&5!vDXT+bt?&=JmhQs%TIdQZ_J#_fOg9{&zc#Nh4q1PhV=O>Y-xrE$<+ zjB?-A*%?Fz%=~llHr}oKVN!qmMYV-Pv8IFKaJe@hj~H^0zYaw;YUj50Dvw1@^?HA& zZh^w+c-RQC+n_eM`?BeDZv1A-)C+R6#=+WWHQH&OF@G>7Kg-nAcC0o%k_CPhlJ6yQ z__1R49KxRSvp*sPlLMt)#pTC=>+BFL%E1%0T7pWY-rG%iku3K;n7 zD)>&A9Rlz*P-RdZv&35jBq=guMjt4FY&H&a`7U%V@fFnsm_#jEI=pxSEI8eSr^|sr zbS=dsVaVywA`#ODxVA}eL(FtY-qej$dvsvJykGM<+}C&ZxNzP(Tl5k3Sm1nLP7H`j z4uPBnO7h&3!$6Tv)yX7EbU;-X6V*%Ga+d+ndYQZ5><67y^vX?qHTm9XsUI# z3yLe6S|1i~X}d*&BaDYF7SlzWOct8|?9R^%Nio#KbDiBKH**b!>Y>J6;oC{m-=x~t zsw~c=eu&zo13rDB^TR_@1isDt!BG(3SD84-%lggCt9y^`+-CsAQTuoLe(@Wg{n>y< zv7eE)(an)^%gJ?C%g3m(-TL00+k*&P(hDhbvxFqpkG~m%PeaNh1F+J4xs4z>PY9(7 zDH~l_JA;15Z)3XpJ-;H$y#;zzbPp;M7|cxC1DoH`X-l{a?u1#igEb^EeJbGgy*pBH zaDXe8$(_M6gLW@j@l(1R;Z%DVd6n2Ur5yv{XM&gblhIWfQAW!Pbk=@hZPU=X`bk+xmq`RZUF4`?-irQoKe#feJhFWVpfhbp z7(SwvN60)f5`h9$885!JC}>hzQKpoZ0-F)v-W&4%yufSRNDxk$J%nTIZ!W#*8 zGAWh;*NyAKC;rd%=8OIo=M)79Nf3+jC^ytR+nlJ;JyYeWvXgv8AcYl9veu(6edmzdXx2`)A z+2M_uW*zig+fDK-d%1T7Lu7u$On6~w9ySU37drg&RUiCq_+us+^8$uZGnG2%TK`G6 zqRnys8gZ1xfRTj(3m{M<;Ct{T8?ag?8LnG2Q}T~${r1ApE<8Lf;FH-f31J^Dwfd-E z##CDH4|TQ7!>V1sOChzxKlz3^q|dedCu!0;Gu&oHQ8{|W6HQ;L@c{0R0MGUQ9izOO z1s{=xiMJgPFr0AFksHz38NPtK)2?c!=!hYP8oRn=wnX_YklvPzCu>cDM_q7q>=0ug z?ghRe5cs%kfOZV*2B|jxA|;c-m~U(hT=A4tit7d(2pA7~s<9yOmM9dZ@(B5Nz@(?a*S|5_WwAW5#8;mALfWd_Tra4oe4Sq$SZK{n+CLkyBwn z6|k8vc4aAP5C+naE?F$RDL?KT+KDzq2<;4|EbJwSH)tIlUG}Gi^>) zR=F_=> zp*H->)16{Dz!6gB|GjvKKwSHXbtoZYyho*dzQfJeiyEJ(;%T@<8sjdhyXFzq&`}+9A z&oaKli(HUTcQ1)Pd0d2l?Sr6Oj+-0-s?GG-IzR{lC|-GXptEMjtA`uY22C3@qAGa} zhlf0egL)d9237e^o#()YR-2Hya}98Sc&w50^~U|STqt{>{kyge$>eKJ;kWl%%L^Fr zM)eIqU4||=qhx0F7JPX#emACTlxR5$rhW6I+t-Yamh5k~?j^+Q<;7UoB2L{Z8kzcypX#v*U&+)-neC zdOU8H5nj4=6$vY4dYOYbJWmwIMJq22PXbcZps3=JC^d%%VK1{5v|FuDtkwHg_p ziLfH4pZJowjis@^Y|&G=>>ev1dXB zDA*AAJ{t8Lx#9~V^c-9AxY90> z7j0;o<3awMq_Fhw#or5Kkj|)V)EJORWaE$4y90{hS6SZKP|EMc=N{H1Cp9!M1)xMN^$JB zuuXUt`U&8S?t5~3%yU#J&K`>U*STZck+z7-rg6jth88c4^&=MjZNVV}3O#%lP3J2l zY?>u_0^jR^_Qy8nE)EeKYp<~ewMS_^Q+d&|F&9aReSa^);PpQYO!d&IG{!KwvO_Lb zxojcfQYc9qcRzFtZb|*YzF<$D$-CrN#q(Ue@WcP}vLWD^X7vNXol?Y;Wl{C#-Oubw33J z$?Yhuv#g)r4T(IA5?;99!76YjsipMo6lpoS^hQT+14v31X~cv&{{5%Ovzw zsH5KT=65}PammCK+;0=5v>ej?l6D=+$mAA8A4eIjNJUD@=RF^7IV7TXz`~ zyeaudqX=)&uPMmwFZNJ*M#Gxv0Glh1kxfCov;bTjlCwKLaipj$rh1loR3+rD2KfS+ znVL*+UeE9iZ!+{t)9BZTMPI9r%ESb zuX=5T$kQl!hI+9y%`*wyr8!+2!KSv-Q4PiFft`}g%{~%p4Qq~ z=fk&xDz48MQ!*ekmOh?VL6G@ns9xP_RnZ!R{kQm*hQ}L&wpz_??KX$;$5EPgG!0P9 zqi+JN1zNQB=Ui__xItjB=wYG0V*0`5BpM~{Q@!H&$v?Cb&e3_KdEq!$zML+6GOHda zJ=|#U)O-yk6Ity$M?^JA`!S}h2v%(tblLLO?=}UQx4h`Od0B3x>4yQgT^~pSda8+H zevRv&fwoVd-ryJft3W&EgaCh9hnec4blWmZLgWPqU&oX`_sO!%`*^J9HubR=x+;ek zJVe@KkW+m(WIYxCWTnPsJgrx-z8j%}g(59%_XQif>S`v87(;w%t1SmrMrAl(fw9Fn zF5WMgSs+ORsdk=@i)*8~tRnpe`TBGNLlY=M$T5>&YMwO{mS%RQcFyNoxAd3kiX+|k zlHqa{IN%w!%r}Xb=#oEy*&fC5-&DT*LDS^ArCg$$br@dOS-3YG9--3xcJqJysNCD| za<^VLdI3YOjnx)dtGA4w`PetjE7tDzsd#?(Ie$2*$6=eb#&bPB%PN0LQTcc|)ElJ0 zt3NH+dLMGG3ebXGE{|)JHWv3iC!GBJjL@(Y?#e1fD`W1dx~dtE7xm|35d-#UUJbPD zq~Q+Lob%Ps(7mdVDClipPI>Rbi;7~Vld>k`t~9iofF;3MwQWI^1NtI`r-<{~2>~+# zhuRO_=G>qjIJ8y1|GZIpIDA+q*)n-a(}(uaw9stHO0f4_GK7p0aD^J|8ohbWkmGu) z)eVY!4u*_DQC$m8eox(o&QyJRrj)!eZYq2FA7X;h%Nlj)<07caR3b7rb?=U$;ZI=88>HCWz$dA@txw zO2*AFa1g+Z(-{r2L**mVNA*EAYy}F1!&hLnTj_31C_{mXZctv8rF}gb1gVvi@Jog_ zc{g3Su?}{Nm=jXi`5tEta*5dDz>wPwG9ZI~sEqs6w{)%2m$d!5CeI5j9n+*xq-?Ge zA#I4{9xwataM7_+RM(_8-^2G2?dUoWS|_F03))`(-=~r?J)GnZ%Cqpu!wud@4S&kE z6ec9M+n^QZWAF9y_TVc|^SN#FF_ zf$29hmm$h*J;^fNOF4CwU)k1sAtrgnWg;M8Mk%o@m1vRU^&-mqxixg}=xm$zq*#)q zqK~t=`_v`Dtvvk=pMMR)PpGXy3^ns^xTTNQzkIuya1cfwOmyKxG9qq2dQZd;SF4Rr z^w!dJ^6W-k8HyCweggC8TD{u-Q~|&=_RK0GCb?l3Ctr2iLIwl-Xp>6#R4+ShW{hnd zXk+qETRd1ZAd|U8=?BmmFPz&tx42aIkYveVay9XBcvXK%kGqr>J(Ohg0m-b6i{gAW@-&=AIrnE4HoI&{=&m z+fQ+Q-4Xs3UFLNKv1nMH%}ZmNgPM=K@+B?>Z`dr>q(4hK?=4Zd-eZcQ^2R&ysc`Iv zQjGD-AB^UdVEIEX{2=&>No)Pa6}_4Bz=@28v^xF`f_@u#Dp$*2Y_qn+6a=A*p7ZR5 zzdu_Hj8w9|nE1{w$6~lq=t>xa48|%o2 zAvLn10)}%|iSKN6v)bpxG(*2f8j&eqbGQaucdYe&6`NPFlJn6C^03u0$NLEiBqGp4 zu0!k!WdD4c?{D7Ed<(~BOspENbMNFuG3l!xoaYSE^heNb!-@gN_%sDKUW*2&UoU;~ z$az*Xfzg-3yLmorva;63SexG@$RBf6tB1DKxDg&u^RrtTEPjYxJ8*68Lmp;ffnm+0 zGm4CM33fhIYY@Be#>(Z7C-HZR#5jEx-l?)%Y!B78b_sD}A`RM7 zuOP5`h3dN7$pW~o5FS6HWmnyr#6M$jlxdqIWVgE48X9slIR8-+*?B1TRJmRhAxhhGa*gb{T|7bNm2z-;40pxS72N;qUpfpGqqdan7GURn4fhP zie{08-r>awUy6SrNk)#<93s;i2IUqnPx7DTu_z8O7FRN=JVqJZ$ZL0A!df**zL&>^ zwSjA)3yTLi*l4KlMKCP1G>Bw!DyB zw3!xi_*j8MO{kYKS63?R(8ECDmh4&JCWxut?XAU*_^|n#L;G@_{Hf11>YM4=tbOsZ zD1+QL2}%H@&%HG%>Ix~k(NZO~mLh+DZ%MlBSPM~-;$**isaY}_)HOq5iOM*IkAQ8W zj42-#Qa(@L&)L5Is&E0HHYojdT*ctgn+B(CaoeDSQY*l<1tecv3zw}rpfz@ zgK~Sbz$o`qDGYpq-B=eo`LFdo&f!Z^U-)X~9V5KN~nGuN@74o=0hLrNtx^e7ydl-fiKm zx+7;Lk(0ENB0H4~r{NI^OE=!i+aFsQUBc+;O;h1LxtLsyyS;j`e}1Iy(#0w|fzZDZ zEj)XV?(ct^n|&u27U4?C8+?ur!QUG9*vAJl6)>VQuMvlv?09 zH=Ey6J^R9AcnM2ga+GMSL<~;i##f^L_!-60zJfW+(=n1yomsdt>%){-V;T~wy*cBp zBjVWfOvJJmVdwt%?qySjZ-XsX1P))gU6ijcTU)819plvL!*$<e~u^s$xak#;7Ko}~_$cQdi2$#8$E|u=*q_*6BZ&3`Iwwifks}rS~yp{4W z75sar_EY3{Eq}D19K<92aN>35cet$HM7b#wtl7YOHVK(t^jmwo4xiI%$#mRQ=0p;O zc1qdZ>??QrrP5@r|LLM&beBM6Vn!|w6kXB#`uOa94xy~->-PZVriWtu)`5`9gHfA3 zgX_G%eXVkKkIB%)C;pY9ur~Ll26zPbXu@M(?4HoPcfpB$sVYMP$JdV~*$;cMX7PD= ztcFa5^DMZD6<9u$z5uzUu20wSR|H5j5?(vx!q58z-@IRQ>6pB>IkvKJQ#m;Pwa8P$ zU6rnNbEVVeptwD+O_OD4$Y_bx`_uj_Cq~U)n=Npr^Ooz$36u#q>r+u+>LC0eYxD~X zSnZb2x1qaF=5=bScVk-CH{0Y8-L5aV$$`@=VJR4}*h*yZhQj5a7^TUQuNFF8fG>cv z?uTik`NP&3M&{I99r%fIOl!)02J|Gy`9KLfrJx*#K~2e0c~^bEqQsM=);wERzaWO4 z_49t(#zjh*RmIoezQ~`9trIVfiHyJUa|sL`dY2|j>XW-7t7?J8EfMfjtEl4(KPnhBZ-@-=9bLsLV8uXdD#H+~|)19b~*diP~@ zIJLZsa|${EY(0IDOjVLE1+~Z15U^_a^Sf6=QnYh~#|!$Ud`#c1PmJHjp(|77iMh z#;2#{WP$}foZ&(mL%;Gh^Y?9>-YrFwBcdfO%KxgNEo!b04#k3Wps2DNoF|5^Mnvk= zo3L-Zt>nn7bTrhtIds6gfe8pieQeItKAw~;YdYi7`#VX<6QsQNP1<9AWMt#P^6?Y6 zNZvSq9j}i>w{4J64X*oWRMgUH??Y?tK)^Q~nBJyU78w30x}V^D5WO+U@$7+5p?wt7 zyt)Uw`p_W@$pjWt=eXX@GVopRf6 zckGitu2u_as!#W2)?#)7OR~YEK`t{xiw+c=eZ7^ymfcxYx?(r}-MdNmHX~*S@p@Ia z?~ZR*@IFL}pYe8NGnw&V{NVjuhCe__$#Z1`G-C(el%x)%@npYlX&ANc(-by$Qe4_p zWMhcu(MYg3!Yd;tmG-mPzL4Ht{RraU+^b3qCl>9D_$!}8VzZ=XO`T`*OPoZ_oIO8I zfWcub`xKleTCr3EgbhNPD{V*rknei)5Rc3$7-IR^!EX5w@bA{Em+?IOBx{q=S4@LF zx7(cNW1au-2)uf;MzI%8%d71_j<6JtXFMVX3*R;l2(t{m#|#>(@#d3hRWN&`toZ(L z&kYm>S}r!N%cq>kba&s_8<9j-c-)rxP5hF{skc#U4uJvY4Al0UoU%HagbE4Fhyiyk z-+_;T#EyctoNWRuNulOH5FY%fkbH+tFqnuf9^rQ>JN{BXQdU&dyS~ZwsPvLi<9Fv4 z@JKh5ZX0#?NEQ+PJav8!LE*87SJ1~wN;e5#6?xy8z8c?Z3!6g$C$~i}6|t@+-b^1( z6KZ{S+rb-*=UmUCWM}y9j6rFXcl{{0`Y*AJ{2rpAh?KEUPEJ^3?D3SV?~i~{$CkUO0Ck6B;1n6%X@j}_ zgR%Xc%0rp_*i>IAZ*;J9?aL~NXA4n3WNshP#7{Zd`O}1c1xw8EQR>3;;{h?<%9-Yy zRe4)mfB(SS+800Z1{M1W1dkJ@?{#T=ehrAh-t^vGEq*&aruP8`D-nF=N&f*$QaGQv z4b`9?2Pd4X+Ns_B^W*YeM#{p55(mqgO^XC@)-8a?DczizYwL@nR9uJU(^XQb{xph~kNuUkq zNj;DE;4M4EZ-(#$6eVb{%Af7j$nQe}TkfSay6!-+x}O-!0gzM`K=9?hx^#9gB2B#6 zcU1H8{5Jm+b5K$2iI+BJ)cePiW~?15^@jY>@xf zSIrnPMoO5!X@RT9?l!FK6fx1Qls|gR z8*i@kwu|WpqivPN1JuZ9oxefqMT4-LGa1>%BXpuEQ2IoID+2}`q{y1#&3%aLeVgt>M(gpZ>1}8#bpj^Y%}|-w-u%qj0+FzdVTL~= zb5sS+FD>pC9-&*3MJgNXdd<(CiX6~77oGX-0RzW z`tO4CKeUD)CO?HuHeb0%TmzBAl^Rkyb_ZbmIgl~QWvB8{T~KOz(UIKG55oPm#wxn9 zJxAl=oDh%m4RwZqBL>sdFaEU6a{UQcb>K5^k=TZkV3OeaOAwrbzOT4n_3=Lk`Pa+I z5r*D}3$i92lp3JgWAfeu6Ccn8JVssLz{9hrDnje7#kU>)4f9@2$4Jke&qR=B2G#$CtB_8{Hc64bHHuS2=l$>nR@X&B-UuvE|)Am?#6ZD7tVj_R|K2X91 zOp66#seiKL|7)(aZd7DfsALaeWGkLJzuUVJ0NkF$3}OBF?fsbxEN2&anG73~D4^0U z@jN`}gWQZaIS0#SW&Fz9F!(^xJD+EJP-r@+C+b*yy|Gv-M*`Eb4VM9>v>_HOVhZ%m zfGAcTf8!5lwMt)_X>j=z37gxkBGuJ1a4CnaIggdC>GxvabE&U`L|^c8VCl?`q!Zm8 zuQ`y1Eb}zSoI)x3tx{QVzRg*mWjePoP?u(QnQN^()U^~`SwL&7&fSwH6H;Bni?9m4 zj@)XZD&3RLjOROt`-a-y_rH^gEWBD};8@)g{`FjaLiME6MOPht@k#DXyh))^#*J=Q zDXp{%`_hw?Mo?0ZEI(Gc!6-T^J~j*aj5Pj*`KYmsh8S9b?v3*;DKW^xeDws{*dk_(1zpkg7EuLwMjs=<(^w{__0A_V+jU3x;<7eF@p;3 zwk8$lc3+M6DV}A1O^XI?yKa+CU%RObSJ2#v0zQlbgq0LN0>x;AiwvrCk@i%t?XUro zw76!YZq^w!M{|#bic(?Qn7hd6I@s}q)uV1VeRtj<_}!&fTGL>yh58^0mV}Lh4I}nf zNWv|v3yJA%3`^u56{j?H2GT-+)9>O>)VkF?mma`>uI^M=Djjx`6fbnGsNUAl4)VT| zSh5maSQCdrWojNza^dulh-K&i-k4)!W9rHX$*!}-HHW6O7BS~6v*RL7?YI?0Lm`cp zHl~|i9kf`8?jGB})QDBIj;2$DIz^0S@P81}7lpA=@wP~u?IeFV_a2!?X1`*~C+Xtwal}%h` zV>&l*7*J#TIBwJ9H2EDqUg}`~o7p6aNC9bhPvB{4;dni_#)HWbL#Mvd@lp?M za86`JsPhO-&T4lIA&#y`%CCg%uS$r8!m9>{i4#JxDpQgZ*!Tx}MVW~uAA{2PY{r6A z3+@V&sP>$vsM6k?`FnwKs}HqOY&_HDvoL{SE46ng1;t8J+!> zq>6as4CS7ctgVc22*#x`aZ4Rv^0Z8^I&D0#pd*-_xm8?sG9>^c*_f{%8=G6kMAC9 zeaT6V4)uG(UM=1!`Q^ycf^=&k{r3y(fA9Bpx&iy9}5nsrFKz%P|k0!w; zHO*se&Z#nBibTa-hxunDmV7nGu%8e4npStlgk6pNhx`FU23sFK_ zVdqz-d7O1PJcu9=kvRBf#ygKd zT7hK9&AjvHB}Iuk!*`Yje+5oq?oOplQ!=Ss?Fw5c0&RxlQj2&(60vHhsg91=Kx5;3 zkNvdSe5v@b5d62j2!Y2LUZ?r+^ufB9Ren57__tbdw>D6-g?fUZWf?Pw97NH{rmel2 z-q@@9u+-+IFKW`pvW87%&pqaANHNlBjW=I4Y4TjLH)>8Fn@p~7BY{j=jC{pYjoVN1 zSbG#WUN9o|6tG&Rzeimh&XR6ivpugPLDzs8VH3wQ+^$i2*#n#YYe(34jc~a5P#rzs zn%AILr1u3>Us7SKGdNmol&Q3xbimdf)YbID4WHbO;{Sd=OTsL3qY}$9Q zJWFZ$vTs}9RtPn#)E4#D|5-}WV9KY@-xb&2I~0@Top2MShDQl14dR!a89ENK7qc4i zGE)x7F<5Q&*8Qw7@aQL+j5-PNW>c(AC@!opdH!r7EM8>6jSS*b-$nNSwfEieRK8){ zSt(>>RCZCa_YOzM$V~R$D?6KGl#q(-tjw%~tZWj6gp<9=&KAcw9Nzm>+VI;-zOPRjtN;C1X=`-jr@%*s(8tf|}r?u{HDi12Z*VW$uShtTUa`&fD z)0q+9ujNu!8-HwQKrB-=?AKThh}tIEDtFWrV2il$xyRXw=-F+sip{ApMS?ui0dHSY z&){s3F-7Y5XoUTJ%%r1-&r4goRSWjn^DO4&s$5R0&_-x_()R4CB*xYTtJ)x^P|Ot5(RNa!_;SC%@;j53TSX-2!vRN1p5D z`pm&X2A7Vqs-%jqrO*KrnbRsr*{M>`zj7U;zCcqZH8aY_;}UJIQYy!Q6TNq8rt>}~ z{^k+0*m82kppk`lgf-p4j>VmDfmpndef9kZFSkedbQpuA5A6DdQ9!vD#gh#1r)@7q<=#4vlxZ`Q z@(ltXA!9;PvmKJ2gEC!OYDJArnrYs3YTDN8ss*$laUbU+ZG$mEqm6>02j!iD_ z*#$+o!G3(F!et~F1ppQy@A-e@HW6C~gBk09^ASHkpN-HO%q22KWppFDSB!+V>C4iF zu_}!H@8ehxbF+H1TW-CasjVixjRG z^m+|-6uMR_Pnu^Ka_`r!r<3;Kp*TtyILe}taS1pFHWnM~$KYV&_GPE)U%TbDe*OCS z4ioyQ-qM<)*i>EtCm&~!!-3E4(YI_ge612&EBB z*$b~Uf`DT%pkHJeU!x-N{Ru&TlY5O##+oC}0b$>5y>}xmwbQNCovB#_gNOM!61VF2 z3n;Y;j17yuD?^H`ewRs1a|3oXyK67Czok=tO@ufBiiJgFMUIuBc#z;l!Q>pWy@*q$ ze`aB=s%+ftK_Sk!HZ|n*!vlwb+~Z{5*%^t?`xn=XjocoLw#Nc;>TS;_>i;IqkhflA>+) zBL5vtpY)FL-TV!=w_U}aha_b#1)5%u1iM=wN{3BfUpj0k+tueB@zuXtwwIC=9Q&Q*}0f)WM`2`s)2Fqh{>L#|N`n@69iA8t*C{ zu86Bag7Oivq+}y*Io?wl^)ZTpPv5YJ{*7YAI;=@8S=1%rJ|t-{s9UHke7izteZmt9FM!?>EQ{ zJ^ThneB#r(h zzI?5bQi;ETjA?e*_09561LKFX_I>eVHKPs!d9Z}pXGyC{iTq<0a}?H5S-385IobTp zOXc%yLzD+6`}lr6N*if;)Wh5O)#|N4$9%D^1X)<@IE1f1_vb!s=m*pSLSi zd5o(0ecyY<86 z)}kZ6Az`Sbw)!I38R8wCg4QsG$Y9JqX!%CQ0gCF#+vB~NsY4OJyE^?2e1i=cOGoK* zd-50VQ9hGx6BgeWT3f5SBcjStS&egLN2wAcDXf^2iMMt(mWK`ZGu2mO26iWDNRxDZ zaSpWh3g46oyb>!9{}R)uZ(z%w7H9_oLb3bjE6Sc*NjA?6)FGbbg{w}Hyb$vhuLyfi z_|F}nmK{>yLh!=#7;FNP!5`u#{flXQZj$?&gn?}bnneH5OiPGiRi~1C~k!; z-YW3Tn+mI*R&sGr!EUy!{1f*-k!Q954%NScSs(sm%7yLkhlFBA$n4%r*kxMFz-+iyQT)7wAKE= zw!}ESBpF|6N7pSdiiGZ0L)%R+Ju{&Vmp0_)pZmJrtEKVo(xiV<8;z6)EsYvEPyXe+ z6hXlyh&D@|RYxI;e2)7osm>=wgXp&US0fRMsE1C57h2GDA_Jo@CIxj~K0K-r)VIGS z^6hDvLbR*gEUU1R@@vx%pj=R9tkcT8T%h!t+f%w%by;M1fgi}h3i<(|9??x5#)=U z%~UEwxJ7KiP@g|Lzhk_HNqA+dd1V7%dXv~GEui^Rgu^(Ewe)LXhy=VP za%Jr+wY=i*vF4`ZTEdHx-siGHlFuI!%Qc{LVTk&B?(`UCgb_#8rcca9-|qVkcNYrHaIw+pBsJG6IZN zJdh?s9PM%3BusE^cBmU4CtMb6zl(!}pit(i@#Bxw&j_B&x7^FYuNoU@O}K&%Y23wV z4W{#c)#J3M#(oDsiUA}C`2~1UFQzU3=*z%fbCVURcpQTtsD#f)4Q1S|QR01#3E--* zLC#U`(+RO1+pBljGIC)M`b*f`-}`ZeF5xMV(9nOZ??QjG@&^Gv2CP4Z0Op5_i%Uw_ z)cATp^7QoBwd2msgA|>+3c--7fR1c$BGU*biiuV->$<`E#F^dCif#9>sTrnR3MQWT zq?i@q;z-h?u=S5-Q4L1bE*35r`gTx6-EooC_l5S(xG`e6ngC>CK_l)eaTHh^ z+{*5RQ&_g|yCnW(O>3B6T7TW*V*t*I_h>zd0_lIt*wi%81ofUPWj^huyF`=Kn#C>} znh)rDYFIlBOM- z=N}vkvVUC6aM-q}Lc|mTc^t?GfWfaMR}@~Z+K~tuJ{T8Ya`SnE^M_4G00>Ves#Ev| zCi4|1)4{EnOx1H|7?{`^AAS5r^`^?%i=m)6H3?3z#&0X+?+=^;@V|Y!Lh9Lz#4_OM z1s-0(KZyY6+}N?Bu0gBD+rJgEcg5ZL3zY%@#10>!(YdZ(C@+KXV4aU>?d{55aQfR@ z=|rLqo%$Y!i>aRJC2kfC`wm2ha17Tp*RR+*4CkC5{tCqip^b z#K2t8I`r!&1#BKebYl~f>#IV%N2JXc|A4GSRs}%07OkYF_am_YEL94GrZ6;DqRB z^f|OZp+eD^B&id8jzql0oFFJymdy`l0kF+>J92hIDqGEsg+KqIA-Ve>Oc;7~oV}FD zJgvB`yY$L|vmtaMPM|t=`19w}Y#8l-XOjLt_VyM;;&SP%xv&y8aVQJ?){l@^=)9UG zNXGLrkbs8{p{$U>iLg+2+PYj9V8Ga-Xsd{iYDjCPN>t0Os~cO`x+&($^BW@!Jo}hL z)yNgGJ5_C7qtE#@43dObZnBr|vV1Sob^sI)%+F^DiWI1iH2;Iv6M%(7=DFA!@TEGV zxEiaNQ06ZEO-y?#=wi$_ZEf#)HFb4$Jv}`mqjiM$MVSXKCBnD=VDhZer$si;Z06!T zo#(f5XmUTdKzt!99+DJ7ClKNQ8GG{R5)&IJt=xR^a~i=vMjB>-YZ`Qy$wu;?DVu&S zPzVhc^z_EHKVAQMoRI}=2o$tOTIJ1OfUq-_oJ&Zho>iNwm^vZzQ zh0^7oq2sFHFz@f5Fm#;=mNt;!dvXP&`sM%!Fj{@@_ql1qXx*4#u=tNOZPMyV}L*k>1G-x-%J7^eM-B~*uO_*32Ab*rPvhyw{y#O1X_-6z&J$S3c+b}{?bGD0 zq}?IZXI8`ufDg(oeyCb$BnlO^S> zr_y;J*5hi>**Il0-#FpG$YsO)a|3 zw_|LlXa7r^hv+ug-6p4P9sv< z(+dE(V#XnJKmn-ULCjm9jB^e%4#Gj8b2AN(IAuMG+*RY?wFXmH`yVxl=0YJ!G#{j> zzPf6Wz-!f2ZqsYonONbuIjvEkx$oO>HEj4P5U2noPwgals+yaP}^HeDp%OvY!2Y6 zi1DHZ`Ilo5`wMR+ynfAQ(L8x^B6)!?NS#dgO59gRW?qSQJ@i&g=5jckvm$XZAH(I$ zRF+F`%krVEJ&N;j)675rM#Rl_p(}-Vi9q<;>HNxLDicTmu*!EsnJVk45fXgP>tY9t zKNZ{01IJ>|`Y409yB7L=I{P`qvMYJ`11`gfxdH)3HuYenynT`4%H~oyz$Ne5l8VjF z9%$8|(+%!Ko?A9c&c;gy7Q!E$L#+N$r-E+DPTaKR0{LFEN9v88P_G|Di>es?fh(fs zFYtE1vKGTh$J4Kbnf}!@kR|M7je~UOBI)!a+7{Zp139PJr@yUXZJh)n*SlUNyeGX! zI%5*%qu__MPtITJ5z=C9^a^j7zqTvikwN-EWt&!%7FP1!vOBfYmtN=b4`GeJuOBM) zQL=(AQBM_T;A(9Pt+T#Rl#mvL&e-pThWOTNcLz!4Q@spd!z9F$0QRf0(UwoSC?>8n zeLLp7*(32D`L4v3)9>+^!bx?n#Bz&Yh{iC?0-9<}3!#ezyIfu^U*-~Rek+6d8SS`jJJh!0iYW{11}^tkG4 zO?ZlxD5Ucd9QBWWbDQd9M$nAT$N>zAs_Vl4#s->OA1oct+^;X3urXTDE~7#J#v;$I z=n#4ipt-9OkNl^I?K|Ig}{J*U0f zvqM&8_3#~U9OKe@e`%pja$9#<(`j|Ab&GIY?$E&f)gockld4YZr_BA)UhvhZG{>Ve zm6hmeKukt%P9r}TwlrksA8WjEgvwFzgLD?H^!S#T4|6lU(nGFB=x(8=!5RGOH}!c+ zS6B-2{q>K^2unJYN8ccG1;q5vTPd%~o;T80KEb5aWrsrxy>$&*2qreSI2*XFJPtbJ zpM9a#t+p$sDXRYEVOik)Oj<@Y7Y?6#S;PD|y9cIQFVLk4x%pY9-hIOGIO7X#fxp4- zI1#Iz-SXY72>l6F!)w)1{QqVZ=)J68yc???9i2b$Gu?jCx@}9=?vCml4-`%k>wa}? zG;&5(5878QlhAw^DjxkHb?;?EUwL0?uP-0iN;-HeWZcI5oV2LsM>mea&P2w7WMktz zN7*m1@~*^K{)q60jy=jR%JcK@$_aTPv;cp-!2F>knmM3)uB%;SdR~-Zd3ZN>dhLLv z)2FQ2=Vu*it&XJCXw0SqOT^I)5GK2x_m>33`ftraX?-bY>{&s45qw58viNb;xS zGxmt?w~wYx<=TXb>iPWmao}+0V0&hza<~jKQ?OD!kj(s-Ut2f0KHq^-<5i`A%sUZbkxob2O+jg*`&A;i_2 z(7h7>N$4Phi4XIRYQ#=~|8Z@|TK(ozGa_ESw|?OKZt1Y=mD@Y;#4=?n@|p=l&GFDM zF%FUSSrsdZM(EtZvDZAOMfObvr!^VDH|E6+N2y_AWU`%p3pV5{g<7=N;Elzx^V3O_ ziNdXnGic=${w!*rpM}pK zlv9i37ExN*EV9q#C8WRSl7Q_6#d`rvsglQZtPGa9u;X8T%>?yEh{yMX|t z4&jl1(aY+~%nnzR-Jt!DYa?lFGcyw{B0_eZ%c3o{u@7qAT`{lt*Te$ZT(%O^mA26_ zOS7ncD^8W8j!Uw%?rDDK`WLk$Fm9#wF8E;1MC(ySkqTXFbaO-KP63#LzY`rf zhxzT`HIfKoBVPjTTsBft8qdLx|CUm@Q6V^~MMo5?=-f7ZMNt*2Q0mzw{rz5B^?_cL(K%sGM0jqH14?pzTJ zcG18{_W>DFv;WFM@@7(!gTJZlumn@dLfkF&wF>wr&~Ood3DY)4wYuX--%b~ zSW=l7TpSN|q&V8~D7bNRser|tx-j>m_Nk#uA9)GrySzGpnB?NE;f7e8iFmla@!lnO zu7^g@NMv`W`*?RJWWB+vBA}G%yqeZ?1|m^q)VUqDrk)mmVh&RTQ0hxf^kf zdnM26ot+Ljr+d(QoZc;Ien!gXO8Sw-j|#;jRAqy!f(AWf+oA75{Iuu)?D_uIbZr7a zkv=qZE*?A>{#4AulF!ggqz50vATJ{(!#_fP{iPAoSqo#H-cxKC}bt+ zdY>y5e^XBkO?SOeCJZ)IUMKQXP4!WHE;9eU ztNYtaqqhV0$^);ToI(qXuAVRJ3rLDq!oKjwt<|YetopY(b_Udi(DK0neSE7FcUV2M%a5t_gT$YrYgNl? z71+ENExS>Q<7(wF9dQviHg)osZ+xV$zYL2nm&b^@MZoRYeLNoaTDa`|sclA&kCU(q z62A<1u?!!7UUk3Dr!H$_^IF$@*sd@6rz6pX>G^VvHs_(XEz1T`6+Ql)hsRjGQBJ)g*#EPMeVxN-+kvee|S0G(dwBpdj{Ui8Rcrlq?Kw( zzJ&E7+~a|jf)CnDbdJw+7`l=WoE7suw>m?He2UE!)j8bEC*70SB~lyPT9lvct{;Et zAhHv<94)nTXfYA-ztJ1^Q7$XOOdz58$Y;&gMN5iJbO-K;@>#E$|BmbMfvFb`*XT9! zoXThI(lkX?`6CcIphE4f#8CL!o{0+Fg!3smYFqtYBLC83T1p@5?9o-8q`t8ZHfzSB zVAmX$T)1B6y2DL;$*Gx!>1rlZo?^gz5V@4MuXs&+5KBHq&#xIpUGQB_Zi& zbwdI;Z)ifF=T!hsM>gz3hbuecIS$lr4p}aDR*t%Nj3d_`wVpHijC2E;=oG%y+oN;i zHSQyRl?Hu>%xsc+bp|Ca{Z{ymY;E16CLx8DlkOe4A$w~fvG^3wYFoqzt=?lgA!EUp zljQt50Sg_8;r>>yTRop<#o$^T>zL*g#N_gFeOgmO;MzrhL%d?3J>XS{X5 z8f`p4g4t@Pk~vN#=^SPrDKOzv#AMGJa~+^<=}M4m&52F~i*^>eikaIE^*`P}7sv+M zww2;{{ru-6(?^PcLHdSW&Ci(3rHY{K|04wA0>r6GjjP3j@fPs`y`2z@Ep7q2p9FWr zlrNr0pLJotlTQ&K2(ZisMS8i}bqAw}ot_8mUrmZASN|$4d}_zxUJ$x{xHw*02@yAV z--5wLh7tG_`&;XfZx-p-(3I1d$+yeEIdq;1#Qp02MUT!rj>{_g;vbP$3t;mMDYU`wVots&q!vB)RRGKdPnYsHD0Ao$ zi>Tx`wU337;Kj<)+&g zF`e}cK!>X0}pI5~=HMSwe1WR3vzNPd)3ML@p zRVqWTgPC7UB>}yI5{sIX7ymoKk$IS}2qg%R$-=JhK%qms0odp8E5!gtXJN}?>GM=w zkKD&qcaQZ6gRMk62g%O$rmM9!k5_NA{FUV_Rr*VxBFL7Utj`D@v0TPE)a|HHdCilr zh@M(vv_Bn3IC{#5Xg4_w*cb9GjI3u{hob%I`ynTg|G(|yNNFJ7jHXfKXV#DlEogr_ zyIJ6DV=(j&*~{GRcqd|kwjxZ?{&bJYf1?d*fSj-d^M9rx7^nm+L8d?7pE!cQNsR*F zu&<>ROaBuv&<1puJRbVbwuL+hGRob!rSqSF0UMxOTjbGywk;UZ`x_+Du>Z^ix{fg$ l&4>Rqz6{j-|BY$f^WU@lkwyAW19A@h$;&897u_`u`XA{IGz9 + +## Concept + +Previously, the DP (Data Plane) and the CP (Control Plane) are not separate explicitly. + +Although we clearly distinguish the different responsibilities of DP and CP in the documentation, not everyone has correctly deployed APISIX in the production environment. + +Therefore, we introduce new concepts called deployment modes/roles, to help users deploy APISIX easily and safely. + +APISIX under different deployment modes will act differently. + +The table below shows the relationship among deployment modes and roles: + +| Deployment Modes | Role | Description | +|------------------|----------------------------|------------------------------------------------------------------------------------------| +| traditional | traditional | DP + CP are deployed together by default. People need to disable `enable_admin` manually | +| decoupled | data_plane / control_plane | DP and CP are deployed independently. | +| standalone | data_plane | Only DP, load the all configurations from local yaml file | + +## Deployment Modes + +### Traditional + +![traditional](../../../assets/images/deployment-traditional.png) + +In the traditional deployment mode, one instance can be both DP & CP. + +There will be a `conf server` listens on UNIX socket and acts as a proxy between APISIX and etcd. + +Both the DP part and CP part of the instance will connect to the `conf server` via HTTP protocol. + +Here is the example of configuration: + +```yaml title="conf/config.yaml" +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + host: + - http://xxxx + prefix: /apisix + timeout: 30 +``` + +### Decoupled + +![decoupled](../../../assets/images/deployment-cp_and_dp.png) + +The instance deployed as data_plane will: + +1. Fetch configurations from the CP, the default port is 9280 +2. Before the DP service starts, it will perform a health check on all CP addresses + - If all CP addresses are unavailable, the startup fails and an exception message is output to the screen. + - If at least one CP address is available, print the unhealthy CP check result log, and then start the APISIX service. + - If all CP addresses are normal, start the APISIX service normally. +3. Handle user requests. + +Here is the example of configuration: + +```yaml title="conf/config.yaml" +deployment: + role: data_plane + role_data_plane: + config_provider: control_plane + control_plane: + host: + - xxxx:9280 + timeout: 30 + certs: + cert: /path/to/ca-cert + cert_key: /path/to/ca-cert + trusted_ca_cert: /path/to/ca-cert +``` + +The instance deployed as control_plane will: + +1. Listen on 9180 by default, and provide Admin API for Admin user +2. Provide `conf server` which listens on port 9280 by default. Both the DP instances and this CP instance will connect to the `conf server` via HTTPS enforced by mTLS. + +Here is the example of configuration: + +```yaml title="conf/config.yaml" +deployment: + role: control_plane + role_control_plan: + config_provider: etcd + conf_server: + listen: 0.0.0.0:9280 + cert: /path/to/ca-cert + cert_key: /path/to/ca-cert + client_ca_cert: /path/to/ca-cert + etcd: + host: + - https://xxxx + prefix: /apisix + timeout: 30 + certs: + cert: /path/to/ca-cert + cert_key: /path/to/ca-cert + trusted_ca_cert: /path/to/ca-cert +``` + +### Standalone + +In this mode, APISIX is deployed as DP and reads configurations from yaml file in the local file system. + +Here is the example of configuration: + +```yaml title="conf/config.yaml" +deployment: + role: data_plane + role_data_plane: + config_provider: yaml +``` diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index b9ad7bc68448..517fc0f1a37f 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -7,7 +7,8 @@ "items": [ "architecture-design/apisix", "architecture-design/plugin-config", - "architecture-design/debug-mode" + "architecture-design/debug-mode", + "architecture-design/deployment-role" ] }, { From 70ba952ef7123b463af219a036548aa8721255b4 Mon Sep 17 00:00:00 2001 From: mika Date: Thu, 16 Jun 2022 15:06:36 +0800 Subject: [PATCH 22/63] docs: fix err in batch-processor (#7259) --- docs/en/latest/batch-processor.md | 2 +- docs/zh/latest/batch-processor.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/batch-processor.md b/docs/en/latest/batch-processor.md index 2f1a0e878d42..a790dbcd2139 100644 --- a/docs/en/latest/batch-processor.md +++ b/docs/en/latest/batch-processor.md @@ -22,7 +22,7 @@ title: Batch Processor --> The batch processor can be used to aggregate entries(logs/any data) and process them in a batch. -When the batch_max_size is set to zero the processor will execute each entry immediately. Setting the batch max size more +When the batch_max_size is set to 1 the processor will execute each entry immediately. Setting the batch max size more than 1 will start aggregating the entries until it reaches the max size or the timeout expires. ## Configurations diff --git a/docs/zh/latest/batch-processor.md b/docs/zh/latest/batch-processor.md index 09f8d550562b..df6d5972e576 100644 --- a/docs/zh/latest/batch-processor.md +++ b/docs/zh/latest/batch-processor.md @@ -22,7 +22,7 @@ title: 批处理器 --> 批处理器可用于聚合条目(日志/任何数据)并进行批处理。 -当 `batch_max_size` 设置为零时,处理器将立即执行每个条目。将批处理的最大值设置为大于 1 将开始聚合条目,直到达到最大值或超时。 +当 `batch_max_size` 设置为 1 时,处理器将立即执行每个条目。将批处理的最大值设置为大于 1 将开始聚合条目,直到达到最大值或超时。 ## 配置 From 88171f4a48ac4ecd81a3f425ac8aeaf9c63fb6a7 Mon Sep 17 00:00:00 2001 From: feihan <97138894+hf400159@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:42:11 +0800 Subject: [PATCH 23/63] docs: update Chinese opentelemetry docs (#7235) --- docs/en/latest/plugins/opentelemetry.md | 6 +- docs/zh/latest/plugins/opentelemetry.md | 131 ++++++++++++------------ 2 files changed, 68 insertions(+), 69 deletions(-) diff --git a/docs/en/latest/plugins/opentelemetry.md b/docs/en/latest/plugins/opentelemetry.md index 19eacf5caf58..f702ee04f3a8 100644 --- a/docs/en/latest/plugins/opentelemetry.md +++ b/docs/en/latest/plugins/opentelemetry.md @@ -116,7 +116,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "10.110.149.175:8089": 1 + "127.0.0.1:1980": 1 } } }' @@ -126,7 +126,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 To disable the `opentelemetry` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. -```console +```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["GET"], @@ -138,7 +138,7 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "10.110.149.175:8089": 1 + "127.0.0.1:1980": 1 } } }' diff --git a/docs/zh/latest/plugins/opentelemetry.md b/docs/zh/latest/plugins/opentelemetry.md index aa6899df3eaf..bcbe04ede805 100644 --- a/docs/zh/latest/plugins/opentelemetry.md +++ b/docs/zh/latest/plugins/opentelemetry.md @@ -23,42 +23,80 @@ title: opentelemetry ## 描述 -[OpenTelemetry](https://opentelemetry.io/) 提供符合 [OpenTelemetry specification](https://opentelemetry.io/docs/reference/specification/) 协议规范的 Tracing 数据上报。 +`opentelemetry` 插件可用于根据 [OpenTelemetry specification](https://opentelemetry.io/docs/reference/specification/) 协议规范上报 Tracing 数据。 -只支持 `HTTP` 协议,且请求类型为 `application/x-protobuf` 的数据上报,相关协议标准:[OTLP/HTTP Request](https://opentelemetry.io/docs/reference/specification/protocol/otlp/#otlphttp-request). +该插件仅支持二进制编码的 [OLTP over HTTP](https://opentelemetry.io/docs/reference/specification/protocol/otlp/#otlphttp),即请求类型为 `application/x-protobuf` 的数据上报。 ## 属性 -| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | -| ------------ | ------ | ------ | -------- | ------------ | ----------------------------------------------------- | -| sampler | object | 可选 | | | 采样配置 -| sampler.name | string | 可选 | always_off | ["always_on", "always_off", "trace_id_ratio", "parent_base"] | 采样算法,always_on:全采样;always_off:不采样;trace_id_ratio:基于 trace id 的百分比采样;parent_base:如果存在 tracing 上游,则使用上游的采样决定,否则使用配置的采样算法决策 -| sampler.options | object | 可选 | | {fraction = 0, root = {name = "always_off"}} | 采样算法参数 -| sampler.options.fraction | number | 可选 | 0 | [0, 1] | trace_id_ratio 采样算法的百分比 -| sampler.options.root | object | 可选 | {name = "always_off", options = {fraction = 0}} | | parent_base 采样算法在没有上游 tracing 时,会使用 root 采样算法做决策 -| sampler.options.root.name | string | 可选 | always_off | ["always_on", "always_off", "trace_id_ratio"] | 采样算法 -| sampler.options.root.options | object | 可选 | {fraction = 0} | | 采样算法参数 -| sampler.options.root.options.fraction | number | 可选 | 0 | [0, 1] | trace_id_ratio 采样算法的百分比 -| additional_attributes | array[string] | optional | | | 追加到 trace span 的额外属性(变量名为 key,变量值为 value) -| additional_attributes[0] | string | required | | | APISIX or Nginx 变量,例如 `http_header` or `route_id` +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ------------------------------------- | ------------- | ------ | ----------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------- | +| sampler | object | 否 | | | 采样策略。 | +| sampler.name | string | 否 | always_off | ["always_on", "always_off", "trace_id_ratio", "parent_base"] | 采样策略。`always_on`:全采样;`always_off`:不采样;`trace_id_ratio`:基于 trace id 的百分比采样;`parent_base`:如果存在 tracing 上游,则使用上游的采样决定,否则使用配置的采样策略决策。 | +| sampler.options | object | 否 | | {fraction = 0, root = {name = "always_off"}} | 采样策略参数。 | +| sampler.options.fraction | number | 否 | 0 | [0, 1] | `trace_id_ratio` 采样策略的百分比。 | +| sampler.options.root | object | 否 | {name = "always_off", options = {fraction = 0}} | | `parent_base` 采样策略在没有上游 tracing 时,会使用 root 采样策略做决策。 | +| sampler.options.root.name | string | 否 | always_off | ["always_on", "always_off", "trace_id_ratio"] | root 采样策略。 | +| sampler.options.root.options | object | 否 | {fraction = 0} | | root 采样策略参数。 | +| sampler.options.root.options.fraction | number | 否 | 0 | [0, 1] | `trace_id_ratio` root 采样策略的百分比 | +| additional_attributes | array[string] | 否 | | | 追加到 trace span 的额外属性(变量名为 `key`,变量值为 `value`)。 | +| additional_attributes[0] | string | 是 | | | APISIX 或 NGINX 变量,例如:`http_header` 或者 `route_id`。 | + +## 如何设置数据上报 + +你可以通过在 `conf/config.yaml` 中指定配置来设置数据上报: + +| 名称 | 类型 | 默认值 | 描述 | +| ------------------------------------------ | ------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| trace_id_source | enum | random | trace ID 的来源。有效值为:`random` 或 `x-request-id`。当设置为 `x-request-id` 时,`x-request-id` 头的值将用作跟踪 ID。请确保当前请求 ID 是符合 TraceID 规范的:`[0-9a-f]{32}`。 | +| resource | object | | 追加到 trace 的额外 [resource](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md)。 | +| collector | object | {address = "127.0.0.1:4318", request_timeout = 3} | OpenTelemetry Collector 配置。 | +| collector.address | string | 127.0.0.1:4318 | 数据采集服务的地址。 | +| collector.request_timeout | integer | 3 | 数据采集服务上报请求超时时长,单位为秒。 | +| collector.request_headers | object | | 数据采集服务上报请求附加的 HTTP 请求头。 | +| batch_span_processor | object | | trace span 处理器参数配置。 | +| batch_span_processor.drop_on_queue_full | boolean | true | 如果设置为 `true` 时,则在队列排满时删除 span。否则,强制处理批次。| +| batch_span_processor.max_queue_size | integer | 2048 | 处理器缓存队列容量的最大值。 | +| batch_span_processor.batch_timeout | number | 5 | 构造一批 span 超时时间,单位为秒。 | +| batch_span_processor.max_export_batch_size | integer | 256 | 单个批次中要处理的 span 数量。 | +| batch_span_processor.inactive_timeout | number | 2 | 两个处理批次之间的时间间隔,单位为秒。 | + +你可以参考以下示例进行配置: + +```yaml title="./conf/config.yaml" +plugin_attr: + opentelemetry: + resource: + service.name: APISIX + tenant.id: business_id + collector: + address: 192.168.8.211:4318 + request_timeout: 3 + request_headers: + foo: bar + batch_span_processor: + drop_on_queue_full: false + max_queue_size: 6 + batch_timeout: 2 + inactive_timeout: 1 + max_export_batch_size: 2 +``` ## 如何启用 -首先,你需要在 `config.yaml` 里面启用 opentelemetry 插件: +`opentelemetry` 插件默认为禁用状态,你需要在配置文件(`./conf/config.yaml`)中开启该插件: -```yaml -# 加到 config.yaml +```yaml title="./conf/config.yaml" plugins: - ... # plugin you need - opentelemetry ``` -然后重载 APISIX。 - -下面是一个示例,在指定的 route 上开启了 opentelemetry 插件: +开启成功后,可以通过如下命令在指定路由上启用 `opentelemetry` 插件: ```shell -curl 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 ' { "methods": ["GET"], "uris": [ @@ -74,58 +112,19 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 "upstream": { "type": "roundrobin", "nodes": { - "10.110.149.175:8089": 1 + "127.0.0.1:1980": 1 } } }' ``` -## 如何设置数据上报 - -我们可以通过指定 `conf/config.yaml` 中的配置来设置数据上报: - -| 名称 | 类型 | 默认值 | 描述 | -| ------------ | ------ | -------- | ----------------------------------------------------- | -| trace_id_source | enum | random | 合法的取值:`random` 或 `x-request-id`,允许使用当前请求 ID 代替随机 ID 作为新的 TraceID,必须确保当前请求 ID 是符合 TraceID 规范的:`[0-9a-f]{32}` | -| resource | object | | 追加到 trace 的额外 [resource](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) | -| collector | object | {address = "127.0.0.1:4318", request_timeout = 3} | 数据采集服务 | -| collector.address | string | 127.0.0.1:4318 | 数据采集服务地址 | -| collector.request_timeout | integer | 3 | 数据采集服务上报请求超时时长,单位秒 | -| collector.request_headers | object | | 数据采集服务上报请求附加的 HTTP 请求头 | -| batch_span_processor | object | | trace span 处理器参数配置 | -| batch_span_processor.drop_on_queue_full | boolean | true | 当处理器缓存队列慢试,丢弃新到来的 span | -| batch_span_processor.max_queue_size | integer | 2048 | 处理器缓存队列容量最大值 | -| batch_span_processor.batch_timeout | number | 5 | 构造一批 span 超时时长,单位秒 | -| batch_span_processor.max_export_batch_size | integer | 256 | 一批 span 的数量,每次上报的 span 数量 | -| batch_span_processor.inactive_timeout | number | 2 | 每隔多长时间检查是否有一批 span 可以上报,单位秒 | - -配置示例: - -```yaml -plugin_attr: - opentelemetry: - resource: - service.name: APISIX - tenant.id: business_id - collector: - address: 192.168.8.211:4318 - request_timeout: 3 - request_headers: - foo: bar - batch_span_processor: - drop_on_queue_full: false - max_queue_size: 6 - batch_timeout: 2 - inactive_timeout: 1 - max_export_batch_size: 2 -``` - ## 禁用插件 -当你想禁用一条路由/服务上的 opentelemetry 插件的时候,很简单,在插件的配置中把对应的 JSON 配置删除即可,无须重启服务,即刻生效: +当你需要禁用 `opentelemetry` 插件时,可以通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: -```console -$ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 \ +-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' { "methods": ["GET"], "uris": [ @@ -136,7 +135,7 @@ $ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335 "upstream": { "type": "roundrobin", "nodes": { - "10.110.149.175:8089": 1 + "127.0.0.1:1980": 1 } } }' From f946da9c943bf902d0057f3a7de44b3b85ee2ec6 Mon Sep 17 00:00:00 2001 From: soulbird Date: Fri, 17 Jun 2022 10:42:57 +0800 Subject: [PATCH 24/63] chore(ci): upgrade etcd version to 3.5.4 (#7265) --- ci/centos7-ci.sh | 5 +---- ci/pod/docker-compose.common.yml | 6 +++--- docs/en/latest/installation-guide.md | 2 +- docs/zh/latest/installation-guide.md | 2 +- utils/linux-install-etcd-client.sh | 4 ++-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/ci/centos7-ci.sh b/ci/centos7-ci.sh index 0f066e6c1520..543e54514be5 100755 --- a/ci/centos7-ci.sh +++ b/ci/centos7-ci.sh @@ -35,10 +35,7 @@ install_dependencies() { ./utils/linux-install-luarocks.sh # install etcdctl - wget https://github.com/etcd-io/etcd/releases/download/v3.4.18/etcd-v3.4.18-linux-amd64.tar.gz - tar xf etcd-v3.4.18-linux-amd64.tar.gz - cp ./etcd-v3.4.18-linux-amd64/etcdctl /usr/local/bin/ - rm -rf etcd-v3.4.18-linux-amd64 + ./utils/linux-install-etcd-client.sh # install vault cli capabilities install_vault_cli diff --git a/ci/pod/docker-compose.common.yml b/ci/pod/docker-compose.common.yml index ecbdfcaf0a47..9e0394a48bd2 100644 --- a/ci/pod/docker-compose.common.yml +++ b/ci/pod/docker-compose.common.yml @@ -31,7 +31,7 @@ services: - "3380:2380" etcd: - image: bitnami/etcd:3.4.18 + image: bitnami/etcd:3.5.4 restart: unless-stopped env_file: - ci/pod/etcd/env/common.env @@ -42,7 +42,7 @@ services: - "2380:2380" etcd_tls: - image: bitnami/etcd:3.4.18 + image: bitnami/etcd:3.5.4 restart: unless-stopped env_file: - ci/pod/etcd/env/common.env @@ -58,7 +58,7 @@ services: - ./t/certs:/certs etcd_mtls: - image: bitnami/etcd:3.4.18 + image: bitnami/etcd:3.5.4 restart: unless-stopped env_file: - ci/pod/etcd/env/common.env diff --git a/docs/en/latest/installation-guide.md b/docs/en/latest/installation-guide.md index 2384ba31b191..9f3bda5a3282 100644 --- a/docs/en/latest/installation-guide.md +++ b/docs/en/latest/installation-guide.md @@ -185,7 +185,7 @@ It would be installed automatically if you choose the Docker or Helm install met ```shell -ETCD_VERSION='3.4.18' +ETCD_VERSION='3.5.4' 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 && \ diff --git a/docs/zh/latest/installation-guide.md b/docs/zh/latest/installation-guide.md index 8fb3d7f80fc8..14b4e5f1ffd9 100644 --- a/docs/zh/latest/installation-guide.md +++ b/docs/zh/latest/installation-guide.md @@ -188,7 +188,7 @@ APISIX 使用 [etcd](https://github.com/etcd-io/etcd) 作为配置中心进行 ```shell -ETCD_VERSION='3.4.18' +ETCD_VERSION='3.5.4' 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 && \ diff --git a/utils/linux-install-etcd-client.sh b/utils/linux-install-etcd-client.sh index ea323aea41f2..f760b6f1777f 100755 --- a/utils/linux-install-etcd-client.sh +++ b/utils/linux-install-etcd-client.sh @@ -18,14 +18,14 @@ # ETCD_ARCH="amd64" -ETCD_VERSION=${ETCD_VERSION:-'3.4.18'} +ETCD_VERSION=${ETCD_VERSION:-'3.5.4'} ARCH=${ARCH:-`(uname -m | tr '[:upper:]' '[:lower:]')`} if [[ $ARCH == "arm64" ]] || [[ $ARCH == "aarch64" ]]; then ETCD_ARCH="arm64" fi -wget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v3.4.18-linux-${ETCD_ARCH}.tar.gz +wget https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}.tar.gz tar xf etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}.tar.gz sudo cp etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}/etcdctl /usr/local/bin/ rm -rf etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH} From 2ed96c09dc1be80275abb8e8bc6acdc398f7373d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=99=E8=8C=82=E6=9E=97?= <1090568335@qq.com> Date: Fri, 17 Jun 2022 11:07:33 +0800 Subject: [PATCH 25/63] fix: grpc-transcode request support object array (#7231) Co-authored-by: jon.yu --- apisix/plugins/grpc-transcode/util.lua | 16 + t/grpc_server_example/main.go | 25 ++ t/grpc_server_example/proto/helloworld.pb.go | 321 ++++++++++++++---- t/grpc_server_example/proto/helloworld.proto | 15 + .../proto/helloworld_grpc.pb.go | 40 +++ t/grpc_server_example/proto/import.pb.go | 21 +- t/grpc_server_example/proto/src.pb.go | 21 +- t/grpc_server_example/proto/src_grpc.pb.go | 4 + t/plugin/grpc-transcode3.t | 124 +++++++ 9 files changed, 518 insertions(+), 69 deletions(-) create mode 100644 t/plugin/grpc-transcode3.t diff --git a/apisix/plugins/grpc-transcode/util.lua b/apisix/plugins/grpc-transcode/util.lua index de54cdb87984..dc4526195639 100644 --- a/apisix/plugins/grpc-transcode/util.lua +++ b/apisix/plugins/grpc-transcode/util.lua @@ -147,6 +147,22 @@ function _M.map_message(field, default_values, request_table) if ty ~= "enum" and field_type:sub(1, 1) == "." then if request_table[name] == nil then sub = default_values and default_values[name] + elseif core.table.isarray(request_table[name]) then + local sub_array = core.table.new(#request_table[name], 0) + for i, value in ipairs(request_table[name]) do + local sub_array_obj + if type(value) == "table" then + sub_array_obj, err = _M.map_message(field_type, + default_values and default_values[name], value) + if err then + return nil, err + end + else + sub_array_obj = value + end + sub_array[i] = sub_array_obj + end + sub = sub_array else sub, err = _M.map_message(field_type, default_values and default_values[name], request_table[name]) diff --git a/t/grpc_server_example/main.go b/t/grpc_server_example/main.go index 18bda0536d00..1b533582c464 100644 --- a/t/grpc_server_example/main.go +++ b/t/grpc_server_example/main.go @@ -172,6 +172,31 @@ func (s *server) SayHelloBidirectionalStream(stream pb.Greeter_SayHelloBidirecti } } +// SayMultipleHello implements helloworld.GreeterServer +func (s *server) SayMultipleHello(ctx context.Context, in *pb.MultipleHelloRequest) (*pb.MultipleHelloReply, error) { + log.Printf("Received: %v", in.Name) + log.Printf("Enum Gender: %v", in.GetGenders()) + msg := "Hello " + in.Name + + persons := in.GetPersons() + if persons != nil { + for _, person := range persons { + if person.GetName() != "" { + msg += fmt.Sprintf(", name: %v", person.GetName()) + } + if person.GetAge() != 0 { + msg += fmt.Sprintf(", age: %v", person.GetAge()) + } + } + } + + return &pb.MultipleHelloReply{ + Message: msg, + Items: in.GetItems(), + Genders: in.GetGenders(), + }, nil +} + func (s *server) Run(ctx context.Context, in *pb.Request) (*pb.Response, error) { return &pb.Response{Body: in.User.Name + " " + in.Body}, nil } diff --git a/t/grpc_server_example/proto/helloworld.pb.go b/t/grpc_server_example/proto/helloworld.pb.go index 9cb209566825..71b16a3455c6 100644 --- a/t/grpc_server_example/proto/helloworld.pb.go +++ b/t/grpc_server_example/proto/helloworld.pb.go @@ -1,8 +1,10 @@ -// Copyright 2015 gRPC authors. // -// Licensed 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 +// 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 // @@ -11,11 +13,12 @@ // 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. +// // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.6.1 +// protoc-gen-go v1.25.0-devel +// protoc v3.12.4 // source: proto/helloworld.proto package proto @@ -374,6 +377,140 @@ func (x *PlusReply) GetResult() int64 { return 0 } +type MultipleHelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Items []string `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + Genders []Gender `protobuf:"varint,3,rep,packed,name=genders,proto3,enum=helloworld.Gender" json:"genders,omitempty"` + Persons []*Person `protobuf:"bytes,4,rep,name=persons,proto3" json:"persons,omitempty"` +} + +func (x *MultipleHelloRequest) Reset() { + *x = MultipleHelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_helloworld_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MultipleHelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultipleHelloRequest) ProtoMessage() {} + +func (x *MultipleHelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_helloworld_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultipleHelloRequest.ProtoReflect.Descriptor instead. +func (*MultipleHelloRequest) Descriptor() ([]byte, []int) { + return file_proto_helloworld_proto_rawDescGZIP(), []int{5} +} + +func (x *MultipleHelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *MultipleHelloRequest) GetItems() []string { + if x != nil { + return x.Items + } + return nil +} + +func (x *MultipleHelloRequest) GetGenders() []Gender { + if x != nil { + return x.Genders + } + return nil +} + +func (x *MultipleHelloRequest) GetPersons() []*Person { + if x != nil { + return x.Persons + } + return nil +} + +type MultipleHelloReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Items []string `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + Genders []Gender `protobuf:"varint,3,rep,packed,name=genders,proto3,enum=helloworld.Gender" json:"genders,omitempty"` +} + +func (x *MultipleHelloReply) Reset() { + *x = MultipleHelloReply{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_helloworld_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MultipleHelloReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MultipleHelloReply) ProtoMessage() {} + +func (x *MultipleHelloReply) ProtoReflect() protoreflect.Message { + mi := &file_proto_helloworld_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MultipleHelloReply.ProtoReflect.Descriptor instead. +func (*MultipleHelloReply) Descriptor() ([]byte, []int) { + return file_proto_helloworld_proto_rawDescGZIP(), []int{6} +} + +func (x *MultipleHelloReply) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *MultipleHelloReply) GetItems() []string { + if x != nil { + return x.Items + } + return nil +} + +func (x *MultipleHelloReply) GetGenders() []Gender { + if x != nil { + return x.Genders + } + return nil +} + var File_proto_helloworld_proto protoreflect.FileDescriptor var file_proto_helloworld_proto_rawDesc = []byte{ @@ -403,40 +540,63 @@ var file_proto_helloworld_proto_rawDesc = []byte{ 0x0a, 0x01, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x62, 0x22, 0x23, 0x0a, 0x09, 0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x2a, 0x40, 0x0a, 0x06, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x0e, 0x47, - 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, - 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x4d, 0x41, 0x4c, 0x45, 0x10, 0x01, - 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x46, 0x45, 0x4d, 0x41, 0x4c, - 0x45, 0x10, 0x02, 0x32, 0xc0, 0x03, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, - 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, - 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, - 0x38, 0x0a, 0x04, 0x50, 0x6c, 0x75, 0x73, 0x12, 0x17, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, - 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x15, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x50, 0x6c, - 0x75, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x12, 0x53, 0x61, 0x79, - 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x41, 0x66, 0x74, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x61, 0x79, 0x12, - 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, - 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, - 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, - 0x79, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x14, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, - 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, - 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, - 0x01, 0x12, 0x4c, 0x0a, 0x14, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, - 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, - 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x12, - 0x55, 0x0a, 0x1b, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x42, 0x69, 0x64, 0x69, 0x72, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, + 0x74, 0x22, 0x9c, 0x01, 0x0a, 0x14, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x69, + 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, + 0x6c, 0x64, 0x2e, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x07, 0x67, 0x65, 0x6e, 0x64, 0x65, + 0x72, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, + 0x2e, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x52, 0x07, 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x73, + 0x22, 0x72, 0x0a, 0x12, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x2c, 0x0a, 0x07, 0x67, 0x65, 0x6e, 0x64, 0x65, 0x72, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, + 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x52, 0x07, 0x67, 0x65, 0x6e, + 0x64, 0x65, 0x72, 0x73, 0x2a, 0x40, 0x0a, 0x06, 0x47, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x12, + 0x0a, 0x0e, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x4d, 0x41, 0x4c, + 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x4e, 0x44, 0x45, 0x52, 0x5f, 0x46, 0x45, + 0x4d, 0x41, 0x4c, 0x45, 0x10, 0x02, 0x32, 0x98, 0x04, 0x0a, 0x07, 0x47, 0x72, 0x65, 0x65, 0x74, + 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, - 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x00, 0x12, 0x38, 0x0a, 0x04, 0x50, 0x6c, 0x75, 0x73, 0x12, 0x17, 0x2e, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, + 0x2e, 0x50, 0x6c, 0x75, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x12, + 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x41, 0x66, 0x74, 0x65, 0x72, 0x44, 0x65, 0x6c, + 0x61, 0x79, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, + 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x10, 0x53, 0x61, 0x79, 0x4d, 0x75, 0x6c, + 0x74, 0x69, 0x70, 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x20, 0x2e, 0x68, 0x65, 0x6c, + 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, + 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, + 0x6c, 0x65, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x4c, + 0x0a, 0x14, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, + 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x14, + 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, + 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x12, 0x55, 0x0a, 0x1b, 0x53, 0x61, + 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x42, 0x69, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, + 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, + 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, + 0x01, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -452,36 +612,43 @@ func file_proto_helloworld_proto_rawDescGZIP() []byte { } var file_proto_helloworld_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proto_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_proto_helloworld_proto_goTypes = []interface{}{ - (Gender)(0), // 0: helloworld.Gender - (*Person)(nil), // 1: helloworld.Person - (*HelloRequest)(nil), // 2: helloworld.HelloRequest - (*HelloReply)(nil), // 3: helloworld.HelloReply - (*PlusRequest)(nil), // 4: helloworld.PlusRequest - (*PlusReply)(nil), // 5: helloworld.PlusReply + (Gender)(0), // 0: helloworld.Gender + (*Person)(nil), // 1: helloworld.Person + (*HelloRequest)(nil), // 2: helloworld.HelloRequest + (*HelloReply)(nil), // 3: helloworld.HelloReply + (*PlusRequest)(nil), // 4: helloworld.PlusRequest + (*PlusReply)(nil), // 5: helloworld.PlusReply + (*MultipleHelloRequest)(nil), // 6: helloworld.MultipleHelloRequest + (*MultipleHelloReply)(nil), // 7: helloworld.MultipleHelloReply } var file_proto_helloworld_proto_depIdxs = []int32{ - 0, // 0: helloworld.HelloRequest.gender:type_name -> helloworld.Gender - 1, // 1: helloworld.HelloRequest.person:type_name -> helloworld.Person - 0, // 2: helloworld.HelloReply.gender:type_name -> helloworld.Gender - 2, // 3: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest - 4, // 4: helloworld.Greeter.Plus:input_type -> helloworld.PlusRequest - 2, // 5: helloworld.Greeter.SayHelloAfterDelay:input_type -> helloworld.HelloRequest - 2, // 6: helloworld.Greeter.SayHelloServerStream:input_type -> helloworld.HelloRequest - 2, // 7: helloworld.Greeter.SayHelloClientStream:input_type -> helloworld.HelloRequest - 2, // 8: helloworld.Greeter.SayHelloBidirectionalStream:input_type -> helloworld.HelloRequest - 3, // 9: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply - 5, // 10: helloworld.Greeter.Plus:output_type -> helloworld.PlusReply - 3, // 11: helloworld.Greeter.SayHelloAfterDelay:output_type -> helloworld.HelloReply - 3, // 12: helloworld.Greeter.SayHelloServerStream:output_type -> helloworld.HelloReply - 3, // 13: helloworld.Greeter.SayHelloClientStream:output_type -> helloworld.HelloReply - 3, // 14: helloworld.Greeter.SayHelloBidirectionalStream:output_type -> helloworld.HelloReply - 9, // [9:15] is the sub-list for method output_type - 3, // [3:9] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 0, // 0: helloworld.HelloRequest.gender:type_name -> helloworld.Gender + 1, // 1: helloworld.HelloRequest.person:type_name -> helloworld.Person + 0, // 2: helloworld.HelloReply.gender:type_name -> helloworld.Gender + 0, // 3: helloworld.MultipleHelloRequest.genders:type_name -> helloworld.Gender + 1, // 4: helloworld.MultipleHelloRequest.persons:type_name -> helloworld.Person + 0, // 5: helloworld.MultipleHelloReply.genders:type_name -> helloworld.Gender + 2, // 6: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest + 4, // 7: helloworld.Greeter.Plus:input_type -> helloworld.PlusRequest + 2, // 8: helloworld.Greeter.SayHelloAfterDelay:input_type -> helloworld.HelloRequest + 6, // 9: helloworld.Greeter.SayMultipleHello:input_type -> helloworld.MultipleHelloRequest + 2, // 10: helloworld.Greeter.SayHelloServerStream:input_type -> helloworld.HelloRequest + 2, // 11: helloworld.Greeter.SayHelloClientStream:input_type -> helloworld.HelloRequest + 2, // 12: helloworld.Greeter.SayHelloBidirectionalStream:input_type -> helloworld.HelloRequest + 3, // 13: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply + 5, // 14: helloworld.Greeter.Plus:output_type -> helloworld.PlusReply + 3, // 15: helloworld.Greeter.SayHelloAfterDelay:output_type -> helloworld.HelloReply + 7, // 16: helloworld.Greeter.SayMultipleHello:output_type -> helloworld.MultipleHelloReply + 3, // 17: helloworld.Greeter.SayHelloServerStream:output_type -> helloworld.HelloReply + 3, // 18: helloworld.Greeter.SayHelloClientStream:output_type -> helloworld.HelloReply + 3, // 19: helloworld.Greeter.SayHelloBidirectionalStream:output_type -> helloworld.HelloReply + 13, // [13:20] is the sub-list for method output_type + 6, // [6:13] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name } func init() { file_proto_helloworld_proto_init() } @@ -550,6 +717,30 @@ func file_proto_helloworld_proto_init() { return nil } } + file_proto_helloworld_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MultipleHelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_helloworld_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MultipleHelloReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -557,7 +748,7 @@ func file_proto_helloworld_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_helloworld_proto_rawDesc, NumEnums: 1, - NumMessages: 5, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/t/grpc_server_example/proto/helloworld.proto b/t/grpc_server_example/proto/helloworld.proto index 2e18a467c822..db056fadec25 100644 --- a/t/grpc_server_example/proto/helloworld.proto +++ b/t/grpc_server_example/proto/helloworld.proto @@ -25,6 +25,7 @@ service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} rpc Plus (PlusRequest) returns (PlusReply) {} rpc SayHelloAfterDelay (HelloRequest) returns (HelloReply) {} + rpc SayMultipleHello(MultipleHelloRequest) returns (MultipleHelloReply) {} // Server side streaming. rpc SayHelloServerStream (HelloRequest) returns (stream HelloReply) {} @@ -34,6 +35,7 @@ service Greeter { // Bidirectional streaming. rpc SayHelloBidirectionalStream (stream HelloRequest) returns (stream HelloReply) {} + } enum Gender { @@ -68,3 +70,16 @@ message PlusRequest { message PlusReply { int64 result = 1; } + +message MultipleHelloRequest { + string name = 1; + repeated string items = 2; + repeated Gender genders = 3; + repeated Person persons = 4; +} + +message MultipleHelloReply{ + string message = 1; + repeated string items = 2; + repeated Gender genders = 3; +} diff --git a/t/grpc_server_example/proto/helloworld_grpc.pb.go b/t/grpc_server_example/proto/helloworld_grpc.pb.go index 7d6d8ef8b7df..c0527d7542f8 100644 --- a/t/grpc_server_example/proto/helloworld_grpc.pb.go +++ b/t/grpc_server_example/proto/helloworld_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.12.4 +// source: proto/helloworld.proto package proto @@ -22,6 +26,7 @@ type GreeterClient interface { SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) Plus(ctx context.Context, in *PlusRequest, opts ...grpc.CallOption) (*PlusReply, error) SayHelloAfterDelay(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) + SayMultipleHello(ctx context.Context, in *MultipleHelloRequest, opts ...grpc.CallOption) (*MultipleHelloReply, error) // Server side streaming. SayHelloServerStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (Greeter_SayHelloServerStreamClient, error) // Client side streaming. @@ -65,6 +70,15 @@ func (c *greeterClient) SayHelloAfterDelay(ctx context.Context, in *HelloRequest return out, nil } +func (c *greeterClient) SayMultipleHello(ctx context.Context, in *MultipleHelloRequest, opts ...grpc.CallOption) (*MultipleHelloReply, error) { + out := new(MultipleHelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayMultipleHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *greeterClient) SayHelloServerStream(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (Greeter_SayHelloServerStreamClient, error) { stream, err := c.cc.NewStream(ctx, &Greeter_ServiceDesc.Streams[0], "/helloworld.Greeter/SayHelloServerStream", opts...) if err != nil { @@ -170,6 +184,7 @@ type GreeterServer interface { SayHello(context.Context, *HelloRequest) (*HelloReply, error) Plus(context.Context, *PlusRequest) (*PlusReply, error) SayHelloAfterDelay(context.Context, *HelloRequest) (*HelloReply, error) + SayMultipleHello(context.Context, *MultipleHelloRequest) (*MultipleHelloReply, error) // Server side streaming. SayHelloServerStream(*HelloRequest, Greeter_SayHelloServerStreamServer) error // Client side streaming. @@ -192,6 +207,9 @@ func (UnimplementedGreeterServer) Plus(context.Context, *PlusRequest) (*PlusRepl func (UnimplementedGreeterServer) SayHelloAfterDelay(context.Context, *HelloRequest) (*HelloReply, error) { return nil, status.Errorf(codes.Unimplemented, "method SayHelloAfterDelay not implemented") } +func (UnimplementedGreeterServer) SayMultipleHello(context.Context, *MultipleHelloRequest) (*MultipleHelloReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayMultipleHello not implemented") +} func (UnimplementedGreeterServer) SayHelloServerStream(*HelloRequest, Greeter_SayHelloServerStreamServer) error { return status.Errorf(codes.Unimplemented, "method SayHelloServerStream not implemented") } @@ -268,6 +286,24 @@ func _Greeter_SayHelloAfterDelay_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _Greeter_SayMultipleHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MultipleHelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayMultipleHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayMultipleHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayMultipleHello(ctx, req.(*MultipleHelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Greeter_SayHelloServerStream_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(HelloRequest) if err := stream.RecvMsg(m); err != nil { @@ -360,6 +396,10 @@ var Greeter_ServiceDesc = grpc.ServiceDesc{ MethodName: "SayHelloAfterDelay", Handler: _Greeter_SayHelloAfterDelay_Handler, }, + { + MethodName: "SayMultipleHello", + Handler: _Greeter_SayMultipleHello_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/t/grpc_server_example/proto/import.pb.go b/t/grpc_server_example/proto/import.pb.go index 28fabf3f3726..a5575fdbd396 100644 --- a/t/grpc_server_example/proto/import.pb.go +++ b/t/grpc_server_example/proto/import.pb.go @@ -1,7 +1,24 @@ +// +// 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. +// + // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.6.1 +// protoc-gen-go v1.25.0-devel +// protoc v3.12.4 // source: proto/import.proto package proto diff --git a/t/grpc_server_example/proto/src.pb.go b/t/grpc_server_example/proto/src.pb.go index 8e6a32ae379b..74fa884d122e 100644 --- a/t/grpc_server_example/proto/src.pb.go +++ b/t/grpc_server_example/proto/src.pb.go @@ -1,7 +1,24 @@ +// +// 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. +// + // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.6.1 +// protoc-gen-go v1.25.0-devel +// protoc v3.12.4 // source: proto/src.proto package proto diff --git a/t/grpc_server_example/proto/src_grpc.pb.go b/t/grpc_server_example/proto/src_grpc.pb.go index 01fe1502d489..d4015ed99142 100644 --- a/t/grpc_server_example/proto/src_grpc.pb.go +++ b/t/grpc_server_example/proto/src_grpc.pb.go @@ -1,4 +1,8 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.12.4 +// source: proto/src.proto package proto diff --git a/t/plugin/grpc-transcode3.t b/t/plugin/grpc-transcode3.t new file mode 100644 index 000000000000..a027a84bd9bd --- /dev/null +++ b/t/plugin/grpc-transcode3.t @@ -0,0 +1,124 @@ +# +# 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 rule +--- config + location /t { + content_by_lua_block { + local http = require "resty.http" + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/proto/1', + ngx.HTTP_PUT, + [[{ + "content" : "syntax = \"proto3\"; + package helloworld; + service Greeter { + rpc SayMultipleHello(MultipleHelloRequest) returns (MultipleHelloReply) {} + } + + enum Gender { + GENDER_UNKNOWN = 0; + GENDER_MALE = 1; + GENDER_FEMALE = 2; + } + + message Person { + string name = 1; + int32 age = 2; + } + + message MultipleHelloRequest { + string name = 1; + repeated string items = 2; + repeated Gender genders = 3; + repeated Person persons = 4; + } + + message MultipleHelloReply{ + string message = 1; + }" + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "methods": ["POST"], + "uri": "/grpctest", + "plugins": { + "grpc-transcode": { + "proto_id": "1", + "service": "helloworld.Greeter", + "method": "SayMultipleHello" + } + }, + "upstream": { + "scheme": "grpc", + "type": "roundrobin", + "nodes": { + "127.0.0.1:50051": 1 + } + } + }]] + ) + + if code >= 300 then + ngx.say(body) + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit route +--- request +POST /grpctest +{"name":"world","persons":[{"name":"Joe","age":1},{"name":"Jake","age":2}]} +--- more_headers +Content-Type: application/json +--- response_body chomp +{"message":"Hello world, name: Joe, age: 1, name: Jake, age: 2"} From e6153b6c36a8b599fe724197b5c9137cacf5a588 Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Fri, 17 Jun 2022 11:15:10 +0530 Subject: [PATCH 26/63] docs: update Metric plugin documents (#7188) --- docs/en/latest/plugins/datadog.md | 143 ++++++++++---------- docs/en/latest/plugins/node-status.md | 81 ++++++------ docs/en/latest/plugins/prometheus.md | 181 ++++++++++++++------------ 3 files changed, 214 insertions(+), 191 deletions(-) diff --git a/docs/en/latest/plugins/datadog.md b/docs/en/latest/plugins/datadog.md index be7661533892..2d5be95b0820 100644 --- a/docs/en/latest/plugins/datadog.md +++ b/docs/en/latest/plugins/datadog.md @@ -1,5 +1,11 @@ --- title: datadog +keywords: + - APISIX + - API Gateway + - Plugin + - Datadog +description: This document contains information about the Apache APISIX datadog Plugin. --- -## How to fetch the metric data +## Fetching metrics -We fetch the metric data from the specified url `/apisix/prometheus/metrics`. +You can fetch the metrics from the specified export URI (default: `/apisix/prometheus/metrics`): -``` +```shell curl -i http://127.0.0.1:9091/apisix/prometheus/metrics ``` -Puts this URL address into prometheus, and it will automatically fetch -these metric data. - -For example like this: +You can add this address to Prometheus to fetch the data: ```yaml scrape_configs: - - job_name: 'apisix' + - job_name: "apisix" scrape_interval: 15s # This value will be related to the time range of the rate function in Prometheus QL. The time range in the rate function should be at least twice this value. - metrics_path: '/apisix/prometheus/metrics' + metrics_path: "/apisix/prometheus/metrics" static_configs: - - targets: ['127.0.0.1:9091'] + - targets: ["127.0.0.1:9091"] ``` -And we can check the status at prometheus console: +Now, you will be able to check the status in your Prometheus console: ![checking status on prometheus dashboard](../../../assets/images/plugin/prometheus01.png) ![prometheus apisix in-depth metric view](../../../assets/images/plugin/prometheus02.png) -## How to specify export uri - -We can change the default export uri in the `plugin_attr` section of `conf/config.yaml`. - -| Name | Type | Default | Description | -| ---------- | ------ | ---------------------------- | --------------------------------- | -| export_uri | string | "/apisix/prometheus/metrics" | uri to get the prometheus metrics | - -Here is an example: +## Using Grafana to graph the metrics -```yaml -plugin_attr: - prometheus: - export_uri: /apisix/metrics -``` +Metrics exported by the `prometheus` Plugin can be graphed in Grafana using a drop in dashboard. -## Grafana dashboard - -Metrics exported by the plugin can be graphed in Grafana using a drop in dashboard. - -Downloads [Grafana dashboard meta](https://github.com/apache/apisix/blob/master/docs/assets/other/json/apisix-grafana-dashboard.json) and imports it to Grafana。 - -Or you can goto [Grafana official](https://grafana.com/grafana/dashboards/11719) for `Grafana` meta data. +To set it up, download [Grafana dashboard meta](https://github.com/apache/apisix/blob/master/docs/assets/other/json/apisix-grafana-dashboard.json) and import it in Grafana. Or, you can go to [Grafana official](https://grafana.com/grafana/dashboards/11719) for Grafana metadata. ![Grafana chart-1](../../../assets/images/plugin/grafana-1.png) @@ -153,52 +158,57 @@ Or you can goto [Grafana official](https://grafana.com/grafana/dashboards/11719) ## Available HTTP metrics -* `Status codes`: HTTP status code returned from upstream services. These status code available per service and across all services. +The following metrics are exported by the `prometheus` Plugin: - Attributes: +- Status code: HTTP status code returned from Upstream services. They are available for a single service and across all services. - | Name | Description | - | -------------| --------------------| - | code | The HTTP status code returned by the upstream service. | - | route | The `route_id` of the matched route is request. If it does not match, the default value is an empty string. | - | matched_uri | The `uri` of the route matching the request, if it does not match, the default value is an empty string. | - | matched_host | The `host` of the route that matches the request. If it does not match, the default value is an empty string. | - | service | The `service_id` of the route matched by the request. When the route lacks service_id, the default is `$host`. | - | consumer | The `consumer_name` of the consumer that matches the request. If it does not match, the default value is an empty string. | - | node | The `ip` of the upstream node. | + The available attributes are: -* `Bandwidth`: Total Bandwidth (egress/ingress) flowing through APISIX. The total bandwidth of per service can be counted. + | Name | Description | + |--------------|-------------------------------------------------------------------------------------------------------------------------------| + | code | HTTP status code returned by the upstream service. | + | route | `route_id` of the matched Route with request. Defaults to an empty string if the Routes don't match. | + | matched_uri | `uri` of the Route matching the request. Defaults to an empty string if the Routes don't match. | + | matched_host | `host` of the Route matching the request. Defaults to an empty string if the Routes don't match. | + | service | `service_id` of the Route matching the request. If the Route does not have a `service_id` configured, it defaults to `$host`. | + | consumer | `consumer_name` of the Consumer matching the request. Defaults to an empty string if it does not match. | + | node | IP address of the Upstream node. | - Attributes: +- Bandwidth: Total amount of traffic (ingress and egress) flowing through APISIX. Total bandwidth of a service can also be obtained. - | Name | Description | - | -------------| ------------- | - | type | The type of bandwidth(egress/ingress). | - | route | The `route_id` of the matched route is request. If it does not match, the default value is an empty string.. | - | service | The `service_id` of the route matched by the request. When the route lacks service_id, the default is `$host`. | - | consumer | The `consumer_name` of the consumer that matches the request. If it does not match, the default value is an empty string. | - | node | The `ip` of the upstream node. | + The available attributes are: -* `etcd reachability`: A gauge type with a value of 0 or 1, representing if etcd can be reached by a APISIX or not, where `1` is available, and `0` is unavailable. -* `Connections`: Various Nginx connection metrics like active, reading, writing, and number of accepted connections. -* `Batch process entries`: A gauge type, when we use plugins and the plugin used batch process to send data, such as: sys logger, http logger, sls logger, tcp logger, udp logger and zipkin, then the entries which hasn't been sent in batch process will be counted in the metrics. -* `Latency`: The per service histogram of request time in different dimensions. + | Name | Description | + |----------|-------------------------------------------------------------------------------------------------------------------------------| + | type | Type of traffic (egress/ingress). | + | route | `route_id` of the matched Route with request. Defaults to an empty string if the Routes don't match. | + | service | `service_id` of the Route matching the request. If the Route does not have a `service_id` configured, it defaults to `$host`. | + | consumer | `consumer_name` of the Consumer matching the request. Defaults to an empty string if it does not match. | + | node | IP address of the Upstream node. | + +- etcd reachability: A gauge type representing whether etcd can be reached by APISIX. A value of `1` represents reachable and `0` represents unreachable. +- Connections: Nginx connection metrics like active, reading, writing, and number of accepted connections. +- Batch process entries: A gauge type useful when Plugins like [syslog](./syslog.md), [http-logger](./http-logger.md), [tcp-logger](./tcp-logger.md), [udp-logger](./udp-logger.md), and [zipkin](./zipkin.md) use batch process to send data. Entries that hasn't been sent in batch process will be counted in the metrics. +- Latency: Histogram of the request time per service in different dimensions. - Attributes: + The available attributes are: - | Name | Description | - | ----------| ------------- | - | type | The value can be `apisix`, `upstream` or `request`, which means http latency caused by apisix, upstream, or their sum. | - | service | The `service_id` of the route matched by the request. When the route lacks service_id, the default is `$host`. | - | consumer | The `consumer_name` of the consumer that matches the request. If it does not match, the default value is an empty string. | - | node | The `ip` of the upstream node. | + | Name | Description | + |----------|-------------------------------------------------------------------------------------------------------------------------------------| + | type | Value can be one of `apisix`, `upstream`, or `request`. This translates to latency caused by APISIX, Upstream, or both (their sum). | + | service | `service_id` of the Route matching the request. If the Route does not have a `service_id` configured, it defaults to `$host`. | + | consumer | `consumer_name` of the Consumer matching the request. Defaults to an empty string if it does not match. | + | node | IP address of the Upstream node. | -* `Info`: the information of APISIX node. +- Info: Information about the APISIX node. -Here is the original metric data of APISIX: +Here are the original metrics from APISIX: + +```shell +curl http://127.0.0.1:9091/apisix/prometheus/metrics +``` ```shell -$ curl http://127.0.0.1:9091/apisix/prometheus/metrics # HELP apisix_bandwidth Total bandwidth in bytes consumed per service in Apisix # TYPE apisix_bandwidth counter apisix_bandwidth{type="egress",route="",service="",consumer="",node=""} 8417 @@ -266,8 +276,7 @@ apisix_node_info{hostname="desktop-2022q8f-wsl"} 1 ## Disable Plugin -Remove the corresponding json configuration in the plugin configuration to disable `prometheus`. -APISIX plugins are hot-reloaded, therefore no need to restart APISIX. +To disable the `prometheus` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' From afce02cea506eb4c70f2dd9d5230d18c7ab8b51e Mon Sep 17 00:00:00 2001 From: kwanhur Date: Mon, 20 Jun 2022 09:10:03 +0800 Subject: [PATCH 27/63] feat(cli): display test option when help (#7268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): display test option when help Signed-off-by: kwanhur * Update semantic word Co-authored-by: 罗泽轩 Co-authored-by: 罗泽轩 --- apisix/cli/ops.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 1e27d9a206d5..3e933ddd3e66 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -66,6 +66,7 @@ stop: stop the apisix server quit: stop the apisix server gracefully restart: restart the apisix server reload: reload the apisix server +test: test the generated nginx.conf version: print the version of apisix ]]) end From 067b2eb389ca0255727ff42ce1a0bc1ab70d5498 Mon Sep 17 00:00:00 2001 From: mika Date: Mon, 20 Jun 2022 11:09:04 +0800 Subject: [PATCH 28/63] feat: export some importent params for kafka-client (#7266) --- apisix/plugins/kafka-logger.lua | 9 +++++++++ docs/en/latest/plugins/kafka-logger.md | 4 ++++ docs/zh/latest/plugins/kafka-logger.md | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/apisix/plugins/kafka-logger.lua b/apisix/plugins/kafka-logger.lua index 2947d145e468..cb43ae3db24b 100644 --- a/apisix/plugins/kafka-logger.lua +++ b/apisix/plugins/kafka-logger.lua @@ -83,6 +83,11 @@ local schema = { -- 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}, + -- config for lua-resty-kafka, default value is same as lua-resty-kafka + producer_batch_num = {type = "integer", minimum = 1, default = 200}, + producer_batch_size = {type = "integer", minimum = 0, default = 1048576}, + producer_max_buffering = {type = "integer", minimum = 1, default = 50000}, + producer_time_linger = {type = "integer", minimum = 1, default = 1} }, required = {"broker_list", "kafka_topic"} } @@ -208,6 +213,10 @@ function _M.log(conf, ctx) broker_config["request_timeout"] = conf.timeout * 1000 broker_config["producer_type"] = conf.producer_type broker_config["required_acks"] = conf.required_acks + broker_config["batch_num"] = conf.producer_batch_num + broker_config["batch_size"] = conf.producer_batch_size + broker_config["max_buffering"] = conf.producer_max_buffering + broker_config["flush_time"] = conf.producer_time_linger * 1000 local prod, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_producer, broker_list, broker_config, conf.cluster_name) diff --git a/docs/en/latest/plugins/kafka-logger.md b/docs/en/latest/plugins/kafka-logger.md index e32f64d128f3..c2f50b9bb34b 100644 --- a/docs/en/latest/plugins/kafka-logger.md +++ b/docs/en/latest/plugins/kafka-logger.md @@ -47,6 +47,10 @@ For more info on Batch-Processor in Apache APISIX please refer. | 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.| +| producer_batch_num | integer | optional | 200 | [1,...] | `batch_num` param in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka), merge message and batch send to server, unit is message count | +| producer_batch_size | integer | optional | 1048576 | [0,...] | `batch_size` param in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka), unit is byte | +| producer_max_buffering | integer | optional | 50000 | [1,...] | `max_buffering` param in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka), max buffer size, unit is message count | +| producer_time_linger | integer | optional | 1 | [1,...] | `flush_time` param in [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka), unit is second | The plugin supports the use of batch processors to aggregate and process entries(logs/data) in a batch. This avoids frequent data submissions by the plugin, which by default the batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. For information or custom batch processor parameter settings, see [Batch-Processor](../batch-processor.md#configuration) configuration section. diff --git a/docs/zh/latest/plugins/kafka-logger.md b/docs/zh/latest/plugins/kafka-logger.md index 9257be4f0cbd..302b273f37a8 100644 --- a/docs/zh/latest/plugins/kafka-logger.md +++ b/docs/zh/latest/plugins/kafka-logger.md @@ -47,6 +47,10 @@ title: kafka-logger | 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 模式。| +| producer_batch_num | integer | 可选 | 200 | [1,...] | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的`batch_num`参数,聚合消息批量提交,单位为消息条数 | +| producer_batch_size | integer | 可选 | 1048576 | [0,...] | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的`batch_size`参数,单位为字节 | +| producer_max_buffering | integer | 可选 | 50000 | [1,...] | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的`max_buffering`参数,最大缓冲区,单位为条 | +| producer_time_linger | integer | 可选 | 1 | [1,...] | 对应 [lua-resty-kafka](https://github.com/doujiang24/lua-resty-kafka) 中的`flush_time`参数,单位为秒 | 本插件支持使用批处理器来聚合并批量处理条目(日志/数据)。这样可以避免插件频繁地提交数据,默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据,如需了解或自定义批处理器相关参数设置,请参考 [Batch-Processor](../batch-processor.md#配置) 配置部分。 From ea7dda050720f42208342a5e2ae92c9ac3014278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 20 Jun 2022 14:13:56 +0800 Subject: [PATCH 29/63] fix(traffic-split): the default timeout doesn't match the one in Nginx (#7277) Signed-off-by: spacewander --- apisix/plugins/traffic-split.lua | 6 +--- t/plugin/traffic-split5.t | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/traffic-split.lua b/apisix/plugins/traffic-split.lua index 9ba0997f6f08..38e272b7be66 100644 --- a/apisix/plugins/traffic-split.lua +++ b/apisix/plugins/traffic-split.lua @@ -172,11 +172,7 @@ local function set_upstream(upstream_info, ctx) upstream_host = upstream_info.upstream_host, key = upstream_info.key, nodes = new_nodes, - timeout = { - send = upstream_info.timeout and upstream_info.timeout.send or 15, - read = upstream_info.timeout and upstream_info.timeout.read or 15, - connect = upstream_info.timeout and upstream_info.timeout.connect or 15 - } + timeout = upstream_info.timeout, } local ok, err = upstream.check_schema(up_conf) diff --git a/t/plugin/traffic-split5.t b/t/plugin/traffic-split5.t index 5e2b80ac363e..1de76cea5d42 100644 --- a/t/plugin/traffic-split5.t +++ b/t/plugin/traffic-split5.t @@ -405,3 +405,62 @@ passed } --- response_body 1970, 1970, 1971, 1972 + + + +=== TEST 7: set up traffic-split rule +--- config + location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local data = { + uri = "/server_port", + plugins = { + ["traffic-split"] = { + rules = { { + match = { { + vars = { { "arg_name", "==", "jack" } } + } }, + weighted_upstreams = { { + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1979"] = 1 + }, + }, + } } + } } + } + }, + upstream = { + type = "roundrobin", + nodes = { + ["127.0.0.1:1980"] = 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) + } + } +--- response_body +passed + + + +=== TEST 8: hit and check default timeout +--- http_config +proxy_connect_timeout 12345s; +--- request +GET /server_port?name=jack +--- log_level: debug +--- error_log eval +qr/event timer add: \d+: 12345000:\d+/ +--- error_code: 502 From 089e8a2181cb9872e1be239cafdb60b4f930e347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 20 Jun 2022 14:24:17 +0800 Subject: [PATCH 30/63] feat(deployment): support connecting to etcd via https (#7269) --- .github/workflows/build.yml | 2 +- .github/workflows/centos7-ci.yml | 2 +- apisix/cli/file.lua | 8 --- apisix/cli/ngx_tpl.lua | 2 +- apisix/cli/ops.lua | 5 +- apisix/cli/schema.lua | 5 +- apisix/cli/snippet.lua | 42 +++++++---- apisix/core/etcd.lua | 42 ++++++++--- t/bin/gen_snippet.lua | 51 ++++++++++++++ t/cli/test_deployment_traditional.sh | 20 ++++++ t/deployment/conf_server.t | 100 +++++++++++++++++++++++++++ 11 files changed, 243 insertions(+), 36 deletions(-) create mode 100755 t/bin/gen_snippet.lua create mode 100644 t/deployment/conf_server.t diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd199672e7ad..6834d8651c18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: - linux_openresty_1_17 test_dir: - t/plugin - - t/admin t/cli t/config-center-yaml t/control t/core t/debug t/discovery t/error_page t/misc + - t/admin t/cli t/config-center-yaml t/control t/core t/debug t/deployment t/discovery t/error_page t/misc - t/node t/pubsub t/router t/script t/stream-node t/utils t/wasm t/xds-library t/xrpc runs-on: ${{ matrix.platform }} diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 9b2f8fc81b9d..589e5ed69225 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -29,7 +29,7 @@ jobs: matrix: test_dir: - t/plugin - - t/admin t/cli t/config-center-yaml t/control t/core t/debug t/discovery t/error_page t/misc + - t/admin t/cli t/config-center-yaml t/control t/core t/debug t/deployment t/discovery t/error_page t/misc - t/node t/pubsub t/router t/script t/stream-node t/utils t/wasm t/xds-library steps: diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua index 5bd64a682e96..66600b54b41b 100644 --- a/apisix/cli/file.lua +++ b/apisix/cli/file.lua @@ -237,14 +237,6 @@ function _M.read_yaml_conf(apisix_home) end end - if default_conf.deployment - and default_conf.deployment.role == "traditional" - and default_conf.deployment.etcd - then - default_conf.etcd = default_conf.deployment.etcd - default_conf.etcd.unix_socket_proxy = "unix:./conf/config_listen.sock" - end - if default_conf.apisix.config_center == "yaml" then local apisix_conf_path = profile:yaml_path("apisix") local apisix_conf_yaml, _ = util.read_file(apisix_conf_path) diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 161c530b8d74..f22280766982 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -64,7 +64,7 @@ lua { {% end %} } -{% if (enabled_stream_plugins["prometheus"] or conf_server) and not enable_http then %} +{% if enabled_stream_plugins["prometheus"] and not enable_http then %} http { {% if enabled_stream_plugins["prometheus"] then %} init_worker_by_lua_block { diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index 3e933ddd3e66..9a275648efb9 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -540,7 +540,10 @@ Please modify "admin_key" in conf/config.yaml . proxy_mirror_timeouts = yaml_conf.plugin_attr["proxy-mirror"].timeout end - local conf_server = snippet.generate_conf_server(yaml_conf) + local conf_server, err = snippet.generate_conf_server(env, yaml_conf) + if err then + util.die(err, "\n") + end -- Using template.render local sys_conf = { diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua index ab053a0a727e..db4f47477de5 100644 --- a/apisix/cli/schema.lua +++ b/apisix/cli/schema.lua @@ -43,7 +43,7 @@ local etcd_schema = { key = { type = "string", }, - } + }, }, prefix = { type = "string", @@ -54,7 +54,8 @@ local etcd_schema = { items = { type = "string", pattern = [[^https?://]] - } + }, + minItems = 1, } }, required = {"prefix", "host"} diff --git a/apisix/cli/snippet.lua b/apisix/cli/snippet.lua index 014719511faa..191e3b0ed463 100644 --- a/apisix/cli/snippet.lua +++ b/apisix/cli/snippet.lua @@ -22,19 +22,31 @@ local ipairs = ipairs local _M = {} -function _M.generate_conf_server(conf) +function _M.generate_conf_server(env, conf) if not (conf.deployment and conf.deployment.role == "traditional") then - return nil + return nil, nil end -- we use proxy even the role is traditional so that we can test the proxy in daily dev - local servers = conf.deployment.etcd.host + local etcd = conf.deployment.etcd + local servers = etcd.host + local enable_https = false + local prefix = "https://" + if servers[1]:find(prefix, 1, true) then + enable_https = true + end + -- there is not a compatible way to verify upstream TLS like the one we do in cosocket + -- so here we just ignore it as the verification is already done in the init phase for i, s in ipairs(servers) do - local prefix = "http://" - -- TODO: support https - if s:find(prefix, 1, true) then - servers[i] = s:sub(#prefix + 1) + if (s:find(prefix, 1, true) ~= nil) ~= enable_https then + return nil, "all nodes in the etcd cluster should enable/disable TLS together" + end + + local _, to = s:find("://", 1, true) + if not to then + return nil, "bad etcd endpoint format" end + servers[i] = s:sub(to + 1) end local conf_render = template.compile([[ @@ -44,12 +56,16 @@ function _M.generate_conf_server(conf) {% end %} } server { - listen unix:./conf/config_listen.sock; + listen unix:{* home *}/conf/config_listen.sock; access_log off; location / { - set $upstream_scheme 'http'; - - proxy_pass $upstream_scheme://apisix_conf_backend; + {% if enable_https then %} + proxy_pass https://apisix_conf_backend; + proxy_ssl_server_name on; + proxy_ssl_protocols TLSv1.2 TLSv1.3; + {% else %} + proxy_pass http://apisix_conf_backend; + {% end %} proxy_http_version 1.1; proxy_set_header Connection ""; @@ -57,7 +73,9 @@ function _M.generate_conf_server(conf) } ]]) return conf_render({ - servers = servers + servers = servers, + enable_https = enable_https, + home = env.apisix_home or ".", }) end diff --git a/apisix/core/etcd.lua b/apisix/core/etcd.lua index 9d289bd5d6e5..274b3a9d80e9 100644 --- a/apisix/core/etcd.lua +++ b/apisix/core/etcd.lua @@ -19,15 +19,19 @@ -- -- @module core.etcd -local fetch_local_conf = require("apisix.core.config_local").local_conf -local array_mt = require("apisix.core.json").array_mt -local etcd = require("resty.etcd") -local clone_tab = require("table.clone") -local health_check = require("resty.etcd.health_check") -local ipairs = ipairs -local setmetatable = setmetatable -local string = string -local tonumber = tonumber +local fetch_local_conf = require("apisix.core.config_local").local_conf +local array_mt = require("apisix.core.json").array_mt +local etcd = require("resty.etcd") +local clone_tab = require("table.clone") +local health_check = require("resty.etcd.health_check") +local ipairs = ipairs +local setmetatable = setmetatable +local string = string +local tonumber = tonumber +local ngx_config_prefix = ngx.config.prefix() + + +local is_http = ngx.config.subsystem == "http" local _M = {} @@ -38,7 +42,25 @@ local function new() return nil, nil, err end - local etcd_conf = clone_tab(local_conf.etcd) + local etcd_conf + + if local_conf.deployment + and local_conf.deployment.role == "traditional" + -- we proxy the etcd requests in traditional mode so we can test the CP's behavior in + -- daily development. However, a stream proxy can't be the CP. + -- Hence, generate a HTTP conf server to proxy etcd requests in stream proxy is + -- unnecessary and inefficient. + and is_http + then + local sock_prefix = ngx_config_prefix + etcd_conf = clone_tab(local_conf.deployment.etcd) + etcd_conf.unix_socket_proxy = + "unix:" .. sock_prefix .. "/conf/config_listen.sock" + etcd_conf.host = {"http://127.0.0.1:2379"} + else + etcd_conf = clone_tab(local_conf.etcd) + end + local prefix = etcd_conf.prefix etcd_conf.http_host = etcd_conf.host etcd_conf.host = nil diff --git a/t/bin/gen_snippet.lua b/t/bin/gen_snippet.lua new file mode 100755 index 000000000000..085409b6b5ae --- /dev/null +++ b/t/bin/gen_snippet.lua @@ -0,0 +1,51 @@ +#!/usr/bin/env luajit +-- +-- 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. +-- +-- this script generates Nginx configuration in the test +-- so we can test some features with test-nginx +local pkg_cpath_org = package.cpath +local pkg_path_org = package.path +local pkg_cpath = "deps/lib64/lua/5.1/?.so;deps/lib/lua/5.1/?.so;" +local pkg_path = "deps/share/lua/5.1/?.lua;" +-- modify the load path to load our dependencies +package.cpath = pkg_cpath .. pkg_cpath_org +package.path = pkg_path .. pkg_path_org + + +local file = require("apisix.cli.file") +local schema = require("apisix.cli.schema") +local snippet = require("apisix.cli.snippet") +local yaml_conf, err = file.read_yaml_conf("t/servroot") +if not yaml_conf then + error(err) +end +local ok, err = schema.validate(yaml_conf) +if not ok then + error(err) +end + +local res, err +if arg[1] == "conf_server" then + res, err = snippet.generate_conf_server( + {apisix_home = "t/servroot/"}, + yaml_conf) +end + +if not res then + error(err or "none") +end +print(res) diff --git a/t/cli/test_deployment_traditional.sh b/t/cli/test_deployment_traditional.sh index f6d7d62c981b..89567511848e 100755 --- a/t/cli/test_deployment_traditional.sh +++ b/t/cli/test_deployment_traditional.sh @@ -111,3 +111,23 @@ if grep '\[error\]' logs/error.log; then fi echo "passed: could connect to etcd" + +echo ' +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - http://127.0.0.1:2379 + - https://127.0.0.1:2379 +' > conf/config.yaml + +out=$(make init 2>&1 || true) +if ! echo "$out" | grep 'all nodes in the etcd cluster should enable/disable TLS together'; then + echo "failed: should validate etcd host" + exit 1 +fi + +echo "passed: validate etcd host" diff --git a/t/deployment/conf_server.t b/t/deployment/conf_server.t new file mode 100644 index 000000000000..84b045055116 --- /dev/null +++ b/t/deployment/conf_server.t @@ -0,0 +1,100 @@ +# +# 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/\/1.17.8/) { + plan(skip_all => "require OpenResty 1.19+"); +} else { + plan('no_plan'); +} + +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]"); + } + +}); + +Test::Nginx::Socket::set_http_config_filter(sub { + my $config = shift; + my $snippet = `./t/bin/gen_snippet.lua conf_server`; + $config .= $snippet; + return $config; +}); + +run_tests(); + +__DATA__ + +=== TEST 1: sync in https +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local t = require("lib.test_admin").test + + local consumers, _ = core.config.new("/consumers", { + automatic = true, + item_schema = core.schema.consumer, + }) + + ngx.sleep(0.6) + local idx = consumers.prev_index + + local code, body = t('/apisix/admin/consumers', + ngx.HTTP_PUT, + [[{ + "username": "jobs", + "plugins": { + "basic-auth": { + "username": "jobs", + "password": "678901" + } + } + }]]) + + ngx.sleep(2) + local new_idx = consumers.prev_index + if new_idx > idx then + ngx.say("prev_index updated") + else + ngx.say("prev_index not update") + end + } + } +--- response_body +prev_index updated +--- extra_yaml_config +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - https://127.0.0.1:12379 + tls: + verify: false From c72c08737f31ca815018d672da9261fd719209d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 20 Jun 2022 14:24:45 +0800 Subject: [PATCH 31/63] fix: the argument to usleep should be integer (#7271) Signed-off-by: spacewander --- apisix/core/config_xds.lua | 6 ++---- apisix/core/os.lua | 19 ++++++++++++++++++- t/core/os.t | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/apisix/core/config_xds.lua b/apisix/core/config_xds.lua index e5e452f7eec6..793592b6fb4c 100644 --- a/apisix/core/config_xds.lua +++ b/apisix/core/config_xds.lua @@ -23,6 +23,7 @@ local config_local = require("apisix.core.config_local") local string = require("apisix.core.string") local log = require("apisix.core.log") local json = require("apisix.core.json") +local os = require("apisix.core.os") local ngx_sleep = require("apisix.core.utils").sleep local check_schema = require("apisix.core.schema").check local new_tab = require("table.new") @@ -67,10 +68,7 @@ end ffi.cdef[[ -typedef unsigned int useconds_t; - extern void initial(void* config_zone, void* version_zone); -int usleep(useconds_t usec); ]] local created_obj = {} @@ -323,7 +321,7 @@ function _M.new(key, opts) -- blocking until xds completes initial configuration while true do - C.usleep(0.1) + os.usleep(1000) fetch_version() if latest_version then break diff --git a/apisix/core/os.lua b/apisix/core/os.lua index ae721e883435..4a922d01e43d 100644 --- a/apisix/core/os.lua +++ b/apisix/core/os.lua @@ -23,6 +23,9 @@ local ffi = require("ffi") local ffi_str = ffi.string local ffi_errno = ffi.errno local C = ffi.C +local ceil = math.ceil +local floor = math.floor +local error = error local tostring = tostring local type = type @@ -71,6 +74,20 @@ function _M.setenv(name, value) end +--- +-- sleep blockingly in microseconds +-- +-- @function core.os.usleep +-- @tparam number us The number of microseconds. +local function usleep(us) + if ceil(us) ~= floor(us) then + error("bad microseconds: " .. us) + end + C.usleep(us) +end +_M.usleep = usleep + + local function waitpid_nohang(pid) local res = C.waitpid(pid, nil, WNOHANG) if res == -1 then @@ -86,7 +103,7 @@ function _M.waitpid(pid, timeout) local total = timeout * 1000 * 1000 while step * count < total do count = count + 1 - C.usleep(step) + usleep(step) local ok, err = waitpid_nohang(pid) if err then return nil, err diff --git a/t/core/os.t b/t/core/os.t index dff6c8b3c191..4c99b311af5d 100644 --- a/t/core/os.t +++ b/t/core/os.t @@ -70,3 +70,22 @@ A false false false + + + +=== TEST 3: usleep, bad arguments +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + + for _, c in ipairs({ + {us = 0.1}, + }) do + local ok = pcall(core.os.usleep, c.us) + ngx.say(ok) + end + } + } +--- response_body +false From 556e7327c72dcf1ae4e458313160fdcf46bdf346 Mon Sep 17 00:00:00 2001 From: feihan <97138894+hf400159@users.noreply.github.com> Date: Tue, 21 Jun 2022 09:52:46 +0800 Subject: [PATCH 32/63] docs: udpate prometheus Chinese doc (#7275) --- docs/zh/latest/plugins/prometheus.md | 192 ++++++++++++++------------- 1 file changed, 103 insertions(+), 89 deletions(-) diff --git a/docs/zh/latest/plugins/prometheus.md b/docs/zh/latest/plugins/prometheus.md index cdfa1f8f464d..191ca5387a4c 100644 --- a/docs/zh/latest/plugins/prometheus.md +++ b/docs/zh/latest/plugins/prometheus.md @@ -1,5 +1,11 @@ --- title: prometheus +keywords: + - APISIX + - API Gateway + - Plugin + - Prometheus +description: 本文将介绍 API 网关 Apache APISIX 如何通过 prometheus 插件将 metrics 上报到开源的监控软件 Prometheus。 --- -## 如何提取指标数据 +## 提取指标 -我们可以从指定的 url 中提取指标数据 `/apisix/prometheus/metrics`: +你可以从指定的 URL(默认:`/apisix/prometheus/metrics`)中提取指标数据: ``` curl -i http://127.0.0.1:9091/apisix/prometheus/metrics ``` -把该 uri 地址配置到 prometheus 中去,就会自动完成指标数据提取。 +你可以将该 URI 地址添加到 Prometheus 中来提取指标数据,配置示例如下: -例子如下: - -```yaml +```yaml title="./prometheus.yml" scrape_configs: - job_name: "apisix" - scrape_interval: 15s # 这个值会跟 Prometheus QL 中 rate 函数的时间范围有关系,rate 函数中的时间范围应该至少两倍于该值。 + scrape_interval: 15s # 该值会跟 Prometheus QL 中 rate 函数的时间范围有关系,rate 函数中的时间范围应该至少两倍于该值。 metrics_path: "/apisix/prometheus/metrics" static_configs: - targets: ["127.0.0.1:9091"] ``` -我们也可以在 prometheus 控制台中去检查状态: +现在你可以在 Prometheus 控制台中检查状态: ![checking status on prometheus dashboard](../../../assets/images/plugin/prometheus01.png) ![prometheus apisix in-depth metric view](../../../assets/images/plugin/prometheus02.png) -## 如何修改暴露指标的 uri +## 使用 Grafana 绘制指标 -我们可以在 `conf/config.yaml` 的 `plugin_attr` 修改默认的 uri +`prometheus` 插件导出的指标可以在 Grafana 进行图形化绘制显示。 -| 名称 | 类型 | 默认值 | 描述 | -| ---------- | ------ | ---------------------------- | -------------- | -| export_uri | string | "/apisix/prometheus/metrics" | 暴露指标的 uri | - -配置示例: - -```yaml -plugin_attr: - prometheus: - export_uri: /apisix/metrics -``` - -## Grafana 面板 - -插件导出的指标可以在 Grafana 进行图形化绘制显示。 - -下载 [Grafana dashboard 元数据](https://github.com/apache/apisix/blob/master/docs/assets/other/json/apisix-grafana-dashboard.json) 并导入到 Grafana 中。 +如果需要进行设置,请下载 [APISIX's 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` 元数据。 @@ -152,46 +161,51 @@ plugin_attr: ## 可用的 HTTP 指标 -* `Status codes`: upstream 服务返回的 HTTP 状态码,可以统计到每个服务或所有服务的响应状态码的次数总和。具有的维度: +`prometheus` 插件可以导出以下指标: + +- Status codes: 上游服务返回的 HTTP 状态码,可以统计到每个服务或所有服务的响应状态码的次数总和。属性如下所示: - | 名称 | 描述 | - | -------------| --------------------| - | code | upstream 服务返回的 HTTP 状态码。 | - | route | 请求匹配的 route 的 `route_id`,未匹配,则默认为空字符串。 | - | matched_uri | 请求匹配的 route 的 `uri`,未匹配,则默认为空字符串。 | - | matched_host | 请求匹配的 route 的 `host`,未匹配,则默认为空字符串。 | - | service | 与请求匹配的 route 的 `service_id`。当路由缺少 service_id 时,则默认为 `$host`。 | - | consumer | 与请求匹配的 consumer 的 `consumer_name`。未匹配,则默认为空字符串。 | - | node | 命中的 upstream 节点 `ip`。| + | 名称 | 描述 | + | -------------| ----------------------------------------------------------------------------- | + | code | 上游服务返回的 HTTP 状态码。 | + | route | 与请求匹配的路由的 `route_id`,如果未匹配,则默认为空字符串。 | + | matched_uri | 与请求匹配的路由的 `uri`,如果未匹配,则默认为空字符串。 | + | matched_host | 与请求匹配的路由的 `host`,如果未匹配,则默认为空字符串。 | + | service | 与请求匹配的路由的 `service_id`。当路由缺少 `service_id` 时,则默认为 `$host`。 | + | consumer | 与请求匹配的消费者的 `consumer_name`。如果未匹配,则默认为空字符串。 | + | node | 上游节点 IP 地址。 | -* `Bandwidth`: 流经 APISIX 的总带宽(可分出口带宽和入口带宽),可以统计到每个服务的带宽总和。具有的维度: +- Bandwidth: 经过 APISIX 的总带宽(出口带宽和入口带宽),可以统计到每个服务的带宽总和。属性如下所示: | 名称 | 描述 | | -------------| ------------- | | type | 带宽的类型 (`ingress` 或 `egress`)。 | - | route | 请求匹配的 route 的 `route_id`,未匹配,则默认为空字符串。 | - | service | 与请求匹配的 route 的 `service_id`。当路由缺少 service_id 时,则默认为 `$host`。 | - | consumer | 与请求匹配的 consumer 的 `consumer_name`。未匹配,则默认为空字符串。 | - | node | 命中的 upstream 节点 `ip`。 | + | route | 与请求匹配的路由的 `route_id`,如果未匹配,则默认为空字符串。 | + | service | 与请求匹配的路由的 `service_id`。当路由缺少 `service_id` 时,则默认为 `$host`。 | + | consumer | 与请求匹配的消费者的 `consumer_name`。如果未匹配,则默认为空字符串。 | + | node | 消费者节点 IP 地址。 | -* `etcd reachability`: APISIX 连接 etcd 的可用性,用 0 和 1 来表示,`1` 表示可用,`0` 表示不可用。 -* `Connections`: 各种的 Nginx 连接指标,如 active(正处理的活动连接数),reading(nginx 读取到客户端的 Header 信息数),writing(nginx 返回给客户端的 Header 信息数),已建立的连接数。 -* `Batch process entries`: 批处理未发送数据计数器,当你使用了批处理发送插件,比如:sys logger, http logger, sls logger, tcp logger, udp logger and zipkin,那么你将会在此指标中看到批处理当前尚未发送的数据的数量。 -* `Latency`: 每个服务的请求用时和 APISIX 处理耗时的直方图。具有的维度: +- etcd reachability: APISIX 连接 etcd 的可用性,用 0 和 1 来表示,`1` 表示可用,`0` 表示不可用。 +- Connections: 各种的 NGINX 连接指标,如 `active`(正处理的活动连接数),`reading`(NGINX 读取到客户端的 Header 信息数),writing(NGINX 返回给客户端的 Header 信息数),已建立的连接数。 +- Batch process entries: 批处理未发送数据计数器,当你使用了批处理发送插件,比如:[syslog](./syslog.md), [http-logger](./http-logger.md), [tcp-logger](./tcp-logger.md), [udp-logger](./udp-logger.md), and [zipkin](./zipkin.md),那么你将会在此指标中看到批处理当前尚未发送的数据的数量。 +- Latency: 每个服务的请求用时和 APISIX 处理耗时的直方图。属性如下所示: - | 名称 | 描述 | - | -------------| ------------- | - | type | 该值可以为 `apisix`、`upstream` 和 `request`,分别表示耗时的来源为 APISIX、上游及其总和。 | - | service | 与请求匹配的 route 的 `service_id`。当路由缺少 service_id 时,则默认为 `$host`。 | - | consumer | 与请求匹配的 consumer 的 `consumer_name`。未匹配,则默认为空字符串。 | - | node | 命中的 upstream 节点 `ip`。 | + | 名称 | 描述 | + | -------------| --------------------------------------------------------------------------------------- | + | type | 该值可以是 `apisix`、`upstream` 和 `request`,分别表示耗时的来源是 APISIX、上游以及两者总和。 | + | service | 与请求匹配的路由 的 `service_id`。当路由缺少 `service_id` 时,则默认为 `$host`。 | + | consumer | 与请求匹配的消费者的 `consumer_name`。未匹配,则默认为空字符串。 | + | node | 上游节点的 IP 地址。 | -* `Info`: 当前 APISIX 节点信息。 +- Info: 当前 APISIX 节点信息。 -这里是 APISIX 的原始的指标数据集: +以下是 APISIX 的原始的指标数据集: ```shell -$ curl http://127.0.0.1:9091/apisix/prometheus/metrics +curl http://127.0.0.1:9091/apisix/prometheus/metrics +``` + +``` # HELP apisix_bandwidth Total bandwidth in bytes consumed per service in Apisix # TYPE apisix_bandwidth counter apisix_bandwidth{type="egress",route="",service="",consumer="",node=""} 8417 @@ -254,12 +268,12 @@ apisix_http_latency_bucket{type="upstream",route="1",service="",consumer="",node ... # HELP apisix_node_info Info of APISIX node # TYPE apisix_node_info gauge -apisix_node_info{hostname="desktop-2022q8f-wsl"} 1 +apisix_node_info{hostname="APISIX"} 1 ``` ## 禁用插件 -在插件设置页面中删除相应的 json 配置即可禁用 `prometheus` 插件。APISIX 的插件是热加载的,因此无需重启 APISIX 服务。 +当你需要禁用 `prometheus` 插件时,可以通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: ```shell curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -279,13 +293,13 @@ curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f1 :::info IMPORTANT -该功能要求 Apache APISIX 运行在 [APISIX-Base](../FAQ.md#如何构建-APISIX-Base-环境?) 上。 +该功能要求 APISIX 运行在 [APISIX-Base](../FAQ.md#如何构建-APISIX-Base-环境?) 上。 ::: 我们也可以通过 `prometheus` 插件采集 TCP/UDP 指标。 -首先,确保 `prometheus` 插件已经在你的配置文件(`conf/config.yaml`)中启用: +首先,确保 `prometheus` 插件已经在你的配置文件(`./conf/config.yaml`)中启用: ```yaml title="conf/config.yaml" stream_plugins: @@ -293,7 +307,7 @@ stream_plugins: - prometheus ``` -接着你需要在 stream route 中配置该插件: +接着你需要在 stream 路由中配置该插件: ```shell curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' @@ -312,20 +326,20 @@ curl http://127.0.0.1:9080/apisix/admin/stream_routes/1 -H 'X-API-KEY: edd1c9f03 ## 可用的 TCP/UDP 指标 -以下是把 APISIX 作为 L4 代理时可用的指标: +以下是将 APISIX 作为 L4 代理时可用的指标: -* `Stream Connections`: 路由级别的已处理连接数。具有的维度: +* Stream Connections: 路由级别的已处理连接数。具有的维度: - | 名称 | 描述 | - | -------------| --------------------| - | route | 匹配的 stream route ID| -* `Connections`: 各种的 Nginx 连接指标,如 active,reading,writing,已建立的连接数。 -* `Info`: 当前 APISIX 节点信息。 + | 名称 | 描述 | + | ------------- | ---------------------- | + | route | 匹配的 stream 路由 ID。 | +* Connections: 各种的 NGINX 连接指标,如 `active`,`reading`,`writing` 等已建立的连接数。 +* Info: 当前 APISIX 节点信息。 -这里是 APISIX 指标的范例: +以下是 APISIX 指标的示例: ```shell -$ curl http://127.0.0.1:9091/apisix/prometheus/metrics +curl http://127.0.0.1:9091/apisix/prometheus/metrics ``` ``` From 9dc15a6a112f79669fbf902e17147a9e8d92208a Mon Sep 17 00:00:00 2001 From: kwanhur Date: Tue, 21 Jun 2022 09:54:06 +0800 Subject: [PATCH 33/63] docs(response-rewrite): change image source from jsdelivr to github (#7193) --- docs/en/latest/plugins/response-rewrite.md | 7 ++++++- docs/zh/latest/plugins/response-rewrite.md | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/en/latest/plugins/response-rewrite.md b/docs/en/latest/plugins/response-rewrite.md index a74bea830905..91739d39ff82 100644 --- a/docs/en/latest/plugins/response-rewrite.md +++ b/docs/en/latest/plugins/response-rewrite.md @@ -126,7 +126,12 @@ However, if `ngx.exit` is executed during an access phase, it will only interrup So, if you have configured the `response-rewrite` Plugin, it do a force overwrite of the response. -![ngx.edit tabular overview](https://cdn.jsdelivr.net/gh/Miss-you/img/picgo/20201113010623.png) +| Phase | rewrite | access | header_filter | body_filter | +|---------------|----------|----------|---------------|-------------| +| rewrite | ngx.exit | √ | √ | √ | +| access | × | ngx.exit | √ | √ | +| header_filter | √ | √ | ngx.exit | √ | +| body_filter | √ | √ | × | ngx.exit | ::: diff --git a/docs/zh/latest/plugins/response-rewrite.md b/docs/zh/latest/plugins/response-rewrite.md index 8cf1b379a1eb..c89b0256b4c8 100644 --- a/docs/zh/latest/plugins/response-rewrite.md +++ b/docs/zh/latest/plugins/response-rewrite.md @@ -125,7 +125,12 @@ X-Server-balancer_addr: 127.0.0.1:80 如果你在 `access` 阶段执行了 `ngx.exit`,该操作只是中断了请求处理阶段,响应阶段仍然会处理。如果你配置了 `response-rewrite` 插件,它会强制覆盖你的响应信息(如响应代码)。 -![ngx.edit tabular overview](https://cdn.jsdelivr.net/gh/Miss-you/img/picgo/20201113010623.png) +| Phase | rewrite | access | header_filter | body_filter | +|---------------|----------|----------|---------------|-------------| +| rewrite | ngx.exit | √ | √ | √ | +| access | × | ngx.exit | √ | √ | +| header_filter | √ | √ | ngx.exit | √ | +| body_filter | √ | √ | × | ngx.exit | ::: From fdd1cac7328879cb848f93bba4d3bb37c50dcdc7 Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Tue, 21 Jun 2022 07:30:48 +0530 Subject: [PATCH 34/63] docs: create page for "Building APISIX" (#7219) --- docs/en/latest/building-apisix.md | 282 ++++++++++++++++++++++++++++++ docs/en/latest/config.json | 10 ++ 2 files changed, 292 insertions(+) create mode 100644 docs/en/latest/building-apisix.md diff --git a/docs/en/latest/building-apisix.md b/docs/en/latest/building-apisix.md new file mode 100644 index 000000000000..fa08d72b590c --- /dev/null +++ b/docs/en/latest/building-apisix.md @@ -0,0 +1,282 @@ +--- +id: building-apisix +title: Building APISIX from source +keywords: + - API gateway + - Apache APISIX + - Code Contribution + - Building APISIX +description: Guide for building and running APISIX locally for development. +--- + + + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +If you are looking to contribute to APISIX or setup a development environment, this guide is for you. + +If you are looking to install and run APISIX, check out the [Installation](/docs/apisix/how-to-build) docs. + +:::note + +If you want to build and package APISIX for a specific platform, see [apisix-build-tools](https://github.com/api7/apisix-build-tools). + +::: + +## Building APISIX from source + +To start, you have to install some dependencies. APISIX provides a handy script to get these installed: + +```shell +curl https://raw.githubusercontent.com/apache/apisix/master/utils/install-dependencies.sh -sL | bash - +``` + +Then, create a directory and set the environment variable `APISIX_VERSION`: + +```shell +APISIX_VERSION='2.14.1' +mkdir apisix-${APISIX_VERSION} +``` + +You can now download the APISIX source code by running the command below: + +```shell +wget https://downloads.apache.org/apisix/${APISIX_VERSION}/apache-apisix-${APISIX_VERSION}-src.tgz +``` + +You can also download the source package from the [Downloads page](https://apisix.apache.org/downloads/). You will also find source packages for APISIX Dashboard and APISIX Ingress Controller. + +After you have downloaded the package, you can extract the files to the folder created previously: + +```shell +tar zxvf apache-apisix-${APISIX_VERSION}-src.tgz -C apisix-${APISIX_VERSION} +``` + +Now, navigate to the directory, create dependencies, and install APISIX as shown below: + +```shell +cd apisix-${APISIX_VERSION} +make deps +make install +``` + +This will install the runtime dependent Lua libraries and the `apisix` command. + +:::note + +If you get an error message like `Could not find header file for LDAP/PCRE/openssl` while running `make deps`, use this solution. + +`luarocks` supports custom compile-time dependencies (See: [Config file format](https://github.com/luarocks/luarocks/wiki/Config-file-format)). You can use a third-party tool to install the missing packages and add its installation directory to the `luarocks`' variables table. This method works on macOS, Ubuntu, CentOS, and other similar operating systems. + +The solution below is for macOS but it works similarly for other operating systems: + +1. Install `openldap` by running: + + ```shell + brew install openldap + ``` + +2. Locate the installation directory by running: + + ```shell + brew --prefix openldap + ``` + +3. Add this path to the project configuration file by any of the two methods shown below: + 1. You can use the `luarocks config` command to set `LDAP_DIR`: + + ```shell + luarocks config variables.LDAP_DIR /opt/homebrew/cellar/openldap/2.6.1 + ``` + + 2. You can also change the default configuration file of `luarocks`. Open the file `~/.luaorcks/config-5.1.lua` and add the following: + + ```shell + variables = { LDAP_DIR = "/opt/homebrew/cellar/openldap/2.6.1", LDAP_INCDIR = "/opt/homebrew/cellar/openldap/2.6.1/include", } + ``` + + `/opt/homebrew/cellar/openldap/` is default path `openldap` is installed on Apple Silicon macOS machines. For Intel machines, the default path is `/usr/local/opt/openldap/`. + +::: + +To uninstall the APISIX runtime, run: + +```shell +make uninstall +make undeps +``` + +:::danger + +This operation will remove the files completely. + +::: + +## Installing etcd + +APISIX uses [etcd](https://github.com/etcd-io/etcd) to save and synchronize configuration. Before running APISIX, you need to install etcd on your machine. Installation methods based on your operating system are mentioned below. + + + + +```shell +ETCD_VERSION='3.4.18' +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 >/tmp/etcd.log 2>&1 & +``` + + + + + +```shell +brew install etcd +brew services start etcd +``` + + + + +## Running and managing APISIX server + +To initialize the configuration file, within the APISIX directory, run: + +```shell +apisix init +``` + +:::tip + +You can run `apisix help` to see a list of available commands. + +::: + +You can then test the created configuration file by running: + +```shell +apisix test +``` + +Finally, you can run the command below to start APISIX: + +```shell +apisix start +``` + +To stop APISIX, you can use either the `quit` or the `stop` subcommand. + +`apisix quit` will gracefully shutdown APISIX. It will ensure that all received requests are completed before stopping. + +```shell +apisix quit +``` + +Where as, the `apisix stop` command does a force shutdown and discards all pending requests. + +```shell +apisix stop +``` + +## Building runtime for APISIX + +Some features of APISIX requires additional Nginx modules to be introduced into OpenResty. + +To use these features, you need to build a custom distribution of OpenResty (apisix-base). See [apisix-build-tools](https://github.com/api7/apisix-build-tools) for setting up your build environment and building it. + +## Running tests + +The steps below show how to run the test cases for APISIX: + +1. Install [cpanminus](https://metacpan.org/pod/App::cpanminus#INSTALLATION), the package manager for Perl. +2. Install the [test-nginx](https://github.com/openresty/test-nginx) dependencies with `cpanm`: + + ```shell + sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) + ``` + +3. Clone the test-nginx source code locally: + + ```shell + git clone https://github.com/openresty/test-nginx.git + ``` + +4. Append the current directory to Perl's module directory by running: + + ```shell + export PERL5LIB=.:$PERL5LIB + ``` + + You can specify the Nginx binary path by running: + + ```shell + TEST_NGINX_BINARY=/usr/local/bin/openresty prove -Itest-nginx/lib -r t + ``` + +5. Run the tests by running: + + ```shell + make test + ``` + +:::note + +Some tests rely on external services and system configuration modification. See [ci/linux_openresty_common_runner.sh](https://github.com/apache/apisix/blob/master/ci/linux_openresty_common_runner.sh) for a complete test environment build. + +::: + +### Troubleshooting + +These are some common troubleshooting steps for running APISIX test cases. + +#### Configuring Nginx path + +For the error `Error unknown directive "lua_package_path" in /API_ASPIX/apisix/t/servroot/conf/nginx.conf`, ensure that OpenResty is set to the default Nginx and export the path as follows: + +- Linux default installation path: + + ```shell + export PATH=/usr/local/openresty/nginx/sbin:$PATH + ``` + +- macOS default installation path (view homebrew): + + ```shell + export PATH=/usr/local/opt/openresty/nginx/sbin:$PATH + ``` + +#### Running a specific test case + +To run a specific test case, use the command below: + +```shell +prove -Itest-nginx/lib -r t/plugin/openid-connect.t +``` + +See [testing framework](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md) for more details. diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 517fc0f1a37f..081ba7ec09da 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -193,6 +193,16 @@ } ] }, + { + "type": "category", + "label": "Development", + "items": [ + { + "type": "doc", + "id": "building-apisix" + } + ] + }, { "type": "doc", "id": "FAQ" From 69bb8b394b8480dd14b518bba5c38970fe7bf24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Tue, 21 Jun 2022 19:38:22 +0800 Subject: [PATCH 35/63] chore: explain why new injected fields should be under `_meta` (#7290) Signed-off-by: spacewander --- apisix/plugin.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index 5aad12e8926c..2276a5c3379f 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -151,6 +151,9 @@ local function load_plugin(name, plugins_list, plugin_type) properties._meta = plugin_injected_schema._meta -- new injected fields should be added under `_meta` + -- 1. so we won't break user's code when adding any new injected fields + -- 2. the semantics is clear, especially in the doc and in the caller side + -- TODO: move the `disable` to `_meta` too plugin.schema['$comment'] = plugin_injected_schema['$comment'] end From 7677b524014d4c9cdec03afc252944dc6dc52e9f Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Wed, 22 Jun 2022 07:23:50 +0530 Subject: [PATCH 36/63] docs: update "Loggers" Plugin 4/n (#7254) --- docs/en/latest/plugins/error-log-logger.md | 127 ++++++++---------- .../en/latest/plugins/google-cloud-logging.md | 67 +++++---- docs/en/latest/plugins/sls-logger.md | 60 ++++----- 3 files changed, 121 insertions(+), 133 deletions(-) diff --git a/docs/en/latest/plugins/error-log-logger.md b/docs/en/latest/plugins/error-log-logger.md index 57fc5732e4a3..cdcf47a2e551 100644 --- a/docs/en/latest/plugins/error-log-logger.md +++ b/docs/en/latest/plugins/error-log-logger.md @@ -1,7 +1,12 @@ --- title: error-log-logger +keywords: + - APISIX + - API Gateway + - Plugin + - Error log logger +description: This document contains information about the Apache APISIX error-log-logger Plugin. --- - & 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%0D<7zJt|IQ`;f1FeaiPax0!`#UGIakb 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 { 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ȩ(l;vYQ@Dft>A#GS1j^AH335RIjne?gG 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 + +## Description + +The `ext-plugin-post-resp` Plugin is for running specific external Plugins in the Plugin Runner before executing the built-in Lua Plugins. + +The `ext-plugin-post-resp` plugin will be executed after the request gets a response from the upstream. + +After enabling this plugin, APISIX will use the [lua-resty-http](https://github.com/api7/lua-resty-http) library to make requests to the upstream, this results in: + +- [proxy-control](./proxy-control.md) plugin is not available +- [proxy-mirror](./proxy-mirror.md) plugin is not available +- [proxy-cache](./proxy-cache.md) plugin is not available +- [mTLS Between APISIX and Upstream](../mtls.md#mtls-between-apisix-and-upstream) function is not available yet + +See [External Plugin](../external-plugin.md) to learn more. + +:::note + +Execution of External Plugins will affect the response of the current request. + +External Plugin does not yet support getting request context information. + +External Plugin does not yet support getting the response body of an upstream response. + +::: + +## Attributes + +| Name | Type | Required | Default | Valid values | Description | +|-------------------|---------|----------|---------|-----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| +| conf | array | False | | [{"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"}] | List of Plugins and their configurations to be executed on the Plugin Runner. | +| allow_degradation | boolean | False | false | | Sets Plugin degradation when the Plugin Runner is not available. When set to `true`, requests are allowed to continue. | + +## Enabling the Plugin + +The example below enables the `ext-plugin-post-resp` Plugin on a specific 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": { + "ext-plugin-post-resp": { + "conf" : [ + {"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"} + ] + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +## Example usage + +Once you have configured the External Plugin as shown above, you can make a request to execute the Plugin: + +```shell +curl -i http://127.0.0.1:9080/index.html +``` + +This will reach the configured Plugin Runner and the `ext-plugin-A` will be executed. + +## Disable Plugin + +To disable the `ext-plugin-post-resp` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. + +```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": { + "127.0.0.1:1980": 1 + } + } +}' +``` diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 940f16015df1..92b2967f3ad7 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -48,7 +48,8 @@ "plugins/real-ip", "plugins/server-info", "plugins/ext-plugin-post-req", - "plugins/ext-plugin-pre-req" + "plugins/ext-plugin-pre-req", + "plugins/ext-plugin-post-resp" ] }, { diff --git a/docs/zh/latest/external-plugin.md b/docs/zh/latest/external-plugin.md index 3e8049f8631d..07b8fbaa35ab 100644 --- a/docs/zh/latest/external-plugin.md +++ b/docs/zh/latest/external-plugin.md @@ -32,6 +32,7 @@ APISIX 支持使用 Lua 语言编写插件,这种类型的插件在 APISIX 内 ![external-plugin](../../assets/images/external-plugin.png) 当你在 APISIX 中配置了一个 Plugin Runner ,APISIX 将以子进程的方式运行该 Plugin Runner 。 + 该子进程与 APISIX 进程从属相同用户。当重启或者重新加载 APISIX 时,该 Plugin Runner 也将被重启。 一旦你为指定路由配置了 `ext-plugin-*` 插件, diff --git a/docs/zh/latest/plugins/ext-plugin-post-resp.md b/docs/zh/latest/plugins/ext-plugin-post-resp.md new file mode 100644 index 000000000000..2027e3e831c7 --- /dev/null +++ b/docs/zh/latest/plugins/ext-plugin-post-resp.md @@ -0,0 +1,111 @@ +--- +title: ext-plugin-post-resp +keywords: + - APISIX + - Plugin + - ext-plugin-post-resp +description: 本文介绍了关于 Apache APISIX `ext-plugin-post-resp` 插件的基本信息及使用方法。 +--- + + + +## 描述 + +`ext-plugin-post-resp` 插件用于在执行内置 Lua 插件之前和在 Plugin Runner 内运行特定的 External Plugin。 + +`ext-plugin-post-resp` 插件将在请求获取到上游的响应之后执行。 + +启用本插件之后,APISIX 将使用 [lua-resty-http](https://github.com/api7/lua-resty-http) 库向上游发起请求,这会导致: + +- [proxy-control](./proxy-control.md) 插件不可用 +- [proxy-mirror](./proxy-mirror.md) 插件不可用 +- [proxy-cache](./proxy-cache.md) 插件不可用 +- [APISIX 与上游间的双向认证](../mtls.md#apisix-与上游间的双向认证) 功能尚不可用 + +如果你想了解更多关于 External Plugin 的信息,请参考 [External Plugin](../external-plugin.md) 。 + +:::note + +External Plugin 执行的结果会影响当前请求的响应。 + +External Plugin 尚不支持获取请求的上下文信息。 + +External Plugin 尚不支持获取上游响应的响应体。 + +::: + +## 属性 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ----------------- | ------ | ------ | ------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| conf | array | 否 | | [{"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"}] | 在 Plugin Runner 内执行的插件列表的配置。 | +| allow_degradation | boolean| 否 | false | [false, true] | 当 Plugin Runner 临时不可用时是否允许请求继续,当值设置为 `true` 时则自动允许请求继续。 | + +## 启用插件 + +以下示例展示了如何在指定路由中启用 `ext-plugin-post-resp` 插件: + +```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-post-resp": { + "conf" : [ + {"name": "ext-plugin-A", "value": "{\"enable\":\"feature\"}"} + ] + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +## 测试插件 + +通过上述命令启用插件后,可以使用如下命令测试插件是否启用成功: + +```shell +curl -i http://127.0.0.1:9080/index.html +``` + +在返回结果中可以看到刚刚配置的 Plugin Runner 已经被触发,同时 `ext-plugin-A` 插件也已经被执行。 + +## 禁用插件 + +当你需要禁用 `ext-plugin-post-resp` 插件时,可通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: + +```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": { + "127.0.0.1:1980": 1 + } + } +}' +``` From a4dd1ac66160f9a6c0e683f8b129fb3569e37ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 23 Jun 2022 15:11:50 +0800 Subject: [PATCH 43/63] feat: ready to release 2.13.2 (#7293) Signed-off-by: spacewander --- CHANGELOG.md | 7 +++ docs/zh/latest/CHANGELOG.md | 5 ++ rockspec/apisix-2.13.2-0.rockspec | 100 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 rockspec/apisix-2.13.2-0.rockspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cd1263eaf6..63e5737651b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ title: Changelog - [2.14.1](#2141) - [2.14.0](#2140) +- [2.13.2](#2132) - [2.13.1](#2131) - [2.13.0](#2130) - [2.12.1](#2121) @@ -120,6 +121,12 @@ title: Changelog - [#6686](https://github.com/apache/apisix/pull/6686) - Admin API rejects unknown stream plugin: [#6813](https://github.com/apache/apisix/pull/6813) +## 2.13.2 + +**This is an LTS maintenance release and you can see the CHANGELOG in `release/2.13` branch.** + +[https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2132](https://github.com/apache/apisix/blob/release/2.13/CHANGELOG.md#2132) + ## 2.13.1 **This is an LTS maintenance release and you can see the CHANGELOG in `release/2.13` branch.** diff --git a/docs/zh/latest/CHANGELOG.md b/docs/zh/latest/CHANGELOG.md index a10464b7024e..08e3a82db38c 100644 --- a/docs/zh/latest/CHANGELOG.md +++ b/docs/zh/latest/CHANGELOG.md @@ -25,6 +25,7 @@ title: CHANGELOG - [2.14.1](#2141) - [2.14.0](#2140) +- [2.13.2](#2132) - [2.13.1](#2131) - [2.13.0](#2130) - [2.12.1](#2121) @@ -120,6 +121,10 @@ title: CHANGELOG - [#6686](https://github.com/apache/apisix/pull/6686) - Admin API 拒绝未知的 stream 插件。[#6813](https://github.com/apache/apisix/pull/6813) +## 2.13.2 + +**这是一个 LTS 维护版本,您可以在 `release/2.13` 分支中看到 CHANGELOG。** + ## 2.13.1 **这是一个 LTS 维护版本,您可以在 `release/2.13` 分支中看到 CHANGELOG。** diff --git a/rockspec/apisix-2.13.2-0.rockspec b/rockspec/apisix-2.13.2-0.rockspec new file mode 100644 index 000000000000..e807d0e4d752 --- /dev/null +++ b/rockspec/apisix-2.13.2-0.rockspec @@ -0,0 +1,100 @@ +-- +-- 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.13.2-0" +supported_platforms = {"linux", "macosx"} + +source = { + url = "git://github.com/apache/apisix", + branch = "2.13.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 = 6.0.2", + "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", + "api7-lua-resty-jwt = 0.2.4", + "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.4", + "lua-resty-openidc = 1.7.2-1", + "luafilesystem = 1.7.0-2", + "api7-lua-tinyyaml = 0.4.2", + "nginx-lua-prometheus = 0.20220127", + "jsonschema = 0.9.8", + "lua-resty-ipmatcher = 0.6.1", + "lua-resty-kafka = 0.07", + "lua-resty-logger-socket = 2.0-0", + "skywalking-nginx-lua = 0.6.0", + "base64 = 1.5-2", + "binaryheap = 0.4", + "api7-dkjson = 0.1.1", + "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.4.0", + "casbin = 1.41.1", + "api7-snowflake = 2.0-1", + "inspect == 3.1.1", + "lualdap = 1.2.6-1", + "lua-resty-rocketmq = 0.3.0-0", + "opentelemetry-lua = 0.1-3", + "net-url = 0.9-1", + "xml2lua = 1.5-2", +} + +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 cbc29a7170aae034bfc2cbbd09f5529bd1968b8d Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Thu, 23 Jun 2022 15:14:01 +0800 Subject: [PATCH 44/63] feat: allows users to specify plugin execution priority (#7273) --- apisix/init.lua | 5 +- apisix/plugin.lua | 62 ++- apisix/schema_def.lua | 4 + docs/en/latest/terminology/plugin.md | 37 ++ docs/zh/latest/terminology/plugin.md | 37 ++ t/admin/plugins.t | 4 +- t/plugin/custom_sort_plugins.t | 633 +++++++++++++++++++++++++++ 7 files changed, 777 insertions(+), 5 deletions(-) create mode 100644 t/plugin/custom_sort_plugins.t diff --git a/apisix/init.lua b/apisix/init.lua index 08899f4ab16d..9cbe6d2049b0 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -445,9 +445,10 @@ function _M.http_access_phase() if changed then api_ctx.matched_route = route core.table.clear(api_ctx.plugins) - api_ctx.plugins = plugin.filter(api_ctx, route, api_ctx.plugins) + local phase = "rewrite_in_consumer" + api_ctx.plugins = plugin.filter(api_ctx, route, api_ctx.plugins, nil, phase) -- rerun rewrite phase for newly added plugins in consumer - plugin.run_plugin("rewrite_in_consumer", api_ctx.plugins, api_ctx) + plugin.run_plugin(phase, api_ctx.plugins, api_ctx) end end plugin.run_plugin("access", plugins, api_ctx) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index 2276a5c3379f..b0344eb0969e 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -68,6 +68,10 @@ local function sort_plugin(l, r) return l.priority > r.priority end +local function custom_sort_plugin(l, r) + return l._meta.priority > r._meta.priority +end + local PLUGIN_TYPE_HTTP = 1 local PLUGIN_TYPE_STREAM = 2 @@ -368,7 +372,7 @@ local function trace_plugins_info_for_debug(ctx, plugins) end -function _M.filter(ctx, conf, plugins, route_conf) +function _M.filter(ctx, conf, plugins, route_conf, phase) local user_plugin_conf = conf.value.plugins if user_plugin_conf == nil or core.table.nkeys(user_plugin_conf) == 0 then @@ -378,6 +382,7 @@ function _M.filter(ctx, conf, plugins, route_conf) return plugins or core.tablepool.fetch("plugins", 0, 0) end + local custom_sort = false local route_plugin_conf = route_conf and route_conf.value.plugins plugins = plugins or core.tablepool.fetch("plugins", 32, 0) for _, plugin_obj in ipairs(local_plugins) do @@ -392,6 +397,9 @@ function _M.filter(ctx, conf, plugins, route_conf) end end + if plugin_conf._meta and plugin_conf._meta.priority then + custom_sort = true + end core.table.insert(plugins, plugin_obj) core.table.insert(plugins, plugin_conf) @@ -401,6 +409,51 @@ function _M.filter(ctx, conf, plugins, route_conf) trace_plugins_info_for_debug(ctx, plugins) + if custom_sort then + local tmp_plugin_objs = core.tablepool.fetch("tmp_plugin_objs", 0, #plugins / 2) + local tmp_plugin_confs = core.tablepool.fetch("tmp_plugin_confs", #plugins / 2, 0) + + for i = 1, #plugins, 2 do + local plugin_obj = plugins[i] + local plugin_conf = plugins[i + 1] + + -- in the rewrite phase, the plugin executes in the following order: + -- 1. execute the rewrite phase of the plugins on route(including the auth plugins) + -- 2. merge plugins from consumer and route + -- 3. execute the rewrite phase of the plugins on consumer(phase: rewrite_in_consumer) + -- in this case, we need to skip the plugins that was already executed(step 1) + if phase == "rewrite_in_consumer" and not plugin_conf._from_consumer then + plugin_conf._skip_rewrite_in_consumer = true + end + + tmp_plugin_objs[plugin_conf] = plugin_obj + core.table.insert(tmp_plugin_confs, plugin_conf) + + if not plugin_conf._meta then + plugin_conf._meta = core.table.new(0, 1) + plugin_conf._meta.priority = plugin_obj.priority + else + if not plugin_conf._meta.priority then + plugin_conf._meta.priority = plugin_obj.priority + end + end + end + + sort_tab(tmp_plugin_confs, custom_sort_plugin) + + local index + for i = 1, #tmp_plugin_confs do + index = i * 2 - 1 + local plugin_conf = tmp_plugin_confs[i] + local plugin_obj = tmp_plugin_objs[plugin_conf] + plugins[index] = plugin_obj + plugins[index + 1] = plugin_conf + end + + core.tablepool.release("tmp_plugin_objs", tmp_plugin_objs) + core.tablepool.release("tmp_plugin_confs", tmp_plugin_confs) + end + return plugins end @@ -757,6 +810,11 @@ function _M.run_plugin(phase, plugins, api_ctx) phase = "rewrite" end local phase_func = plugins[i][phase] + + if phase == "rewrite" and plugins[i + 1]._skip_rewrite_in_consumer then + goto CONTINUE + end + if phase_func then plugin_run = true local conf = plugins[i + 1] @@ -784,6 +842,8 @@ function _M.run_plugin(phase, plugins, api_ctx) end end end + + ::CONTINUE:: end return api_ctx, plugin_run end diff --git a/apisix/schema_def.lua b/apisix/schema_def.lua index 7d39b62aad76..16dccc6d8fa2 100644 --- a/apisix/schema_def.lua +++ b/apisix/schema_def.lua @@ -952,6 +952,10 @@ _M.plugin_injected_schema = { { type = "object" }, } }, + priority = { + description = "priority of plugins by customized order", + type = "integer", + }, } } } diff --git a/docs/en/latest/terminology/plugin.md b/docs/en/latest/terminology/plugin.md index 4bb12a4e3146..6ab969769024 100644 --- a/docs/en/latest/terminology/plugin.md +++ b/docs/en/latest/terminology/plugin.md @@ -95,6 +95,43 @@ the configuration above means customizing the error response from the jwt-auth p | Name | Type | Description | |--------------|------|-------------| | error_response | string/object | Custom error response | +| priority | integer | Custom plugin priority | + +### Custom Plugin Priority + +All plugins have a default priority, but it is possible to customize the plugin priority to change the plugin's execution order. + +```json + { + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + }, + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } +} +``` + +The default priority of serverless-pre-function is 10000, and the default priority of serverless-post-function is -2000. By default, the serverless-pre-function plugin will be executed first, and serverless-post-function plugin will be executed next. + +The above configuration means setting the priority of the serverless-pre-function plugin to -2000 and the priority of the serverless-post-function plugin to 10000. The serverless-post-function plugin will be executed first, and serverless-pre-function plugin will be executed next. + +Note: + +- Custom plugin priority only affects the current object(route, service ...) of the plugin instance binding, not all instances of that plugin. For example, if the above plugin configuration belongs to Route A, the order of execution of the plugins serverless-post-function and serverless-post-function on Route B will not be affected and the default priority will be used. +- Custom plugin priority does not apply to the rewrite phase of some plugins configured on the consumer. The rewrite phase of plugins configured on the route will be executed first, and then the rewrite phase of plugins (exclude auth plugins) from the consumer will be executed. ## Hot Reload diff --git a/docs/zh/latest/terminology/plugin.md b/docs/zh/latest/terminology/plugin.md index 8883ef3744af..86bed6442982 100644 --- a/docs/zh/latest/terminology/plugin.md +++ b/docs/zh/latest/terminology/plugin.md @@ -89,6 +89,43 @@ local _M = { | 名称 | 类型 | 描述 | |--------------|------|----------------| | error_response | string/object | 自定义错误响应 | +| priority | integer | 自定义插件优先级 | + +### 自定义插件优先级 + +所有插件都有默认优先级,但是可以自定义插件优先级来改变插件执行顺序。 + +```json + { + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + }, + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } +} +``` + +serverless-pre-function 的默认优先级是 10000,serverless-post-function 的默认优先级是 -2000。默认情况下会先执行 serverless-pre-function 插件,再执行 serverless-post-function 插件。 + +上面的配置意味着将 serverless-pre-function 插件的优先级设置为 -2000,serverless-post-function 插件的优先级设置为 10000。serverless-post-function 插件会先执行,再执行 serverless-pre-function 插件。 + +注意: + +- 自定义插件优先级只会影响插件实例绑定的主体,不会影响该插件的所有实例。比如上面的插件配置属于路由 A ,路由 B 上的插件 serverless-post-function 和 serverless-post-function 插件执行顺序不会受到影响,会使用默认优先级。 +- 自定义插件优先级不适用于 consumer 上配置的插件的 rewrite 阶段。路由上配置的插件的 rewrite 阶段将会优先运行,然后才会运行 consumer 上除 auth 插件之外的其他插件的 rewrite 阶段。 ## 热加载 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index c370c3c512fb..d7881249d40e 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -265,7 +265,7 @@ plugins: } } --- response_body eval -qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\},"skey":\{"type":"string"\}\},"required":\["ikey","skey"\],"type":"object"\},"priority":0,"schema":\{"\$comment":"this is a mark for our injected plugin schema","properties":\{"_meta":\{"properties":\{"error_response":\{"oneOf":\[\{"type":"string"\},\{"type":"object"\}\]\}\},"type":"object"\},"disable":\{"type":"boolean"\},"i":\{"minimum":0,"type":"number"\},"ip":\{"type":"string"\},"port":\{"type":"integer"\},"s":\{"type":"string"\},"t":\{"minItems":1,"type":"array"\}\},"required":\["i"\],"type":"object"\},"version":0.1\}/ +qr/\{"metadata_schema":\{"properties":\{"ikey":\{"minimum":0,"type":"number"\},"skey":\{"type":"string"\}\},"required":\["ikey","skey"\],"type":"object"\},"priority":0,"schema":\{"\$comment":"this is a mark for our injected plugin schema","properties":\{"_meta":\{"properties":\{"error_response":\{"oneOf":\[\{"type":"string"\},\{"type":"object"\}\]\},"priority":\{"description":"priority of plugins by customized order","type":"integer"\}\},"type":"object"\},"disable":\{"type":"boolean"\},"i":\{"minimum":0,"type":"number"\},"ip":\{"type":"string"\},"port":\{"type":"integer"\},"s":\{"type":"string"\},"t":\{"minItems":1,"type":"array"\}\},"required":\["i"\],"type":"object"\},"version":0.1\}/ @@ -366,7 +366,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":{"_meta":{"properties":{"error_response":{"oneOf":[{"type":"string"},{"type":"object"}]}},"type":"object"},"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} +{"priority":1003,"schema":{"$comment":"this is a mark for our injected plugin schema","properties":{"_meta":{"properties":{"error_response":{"oneOf":[{"type":"string"},{"type":"object"}]},"priority":{"description":"priority of plugins by customized order","type":"integer"}},"type":"object"},"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} diff --git a/t/plugin/custom_sort_plugins.t b/t/plugin/custom_sort_plugins.t new file mode 100644 index 000000000000..41a23b9adbc8 --- /dev/null +++ b/t/plugin/custom_sort_plugins.t @@ -0,0 +1,633 @@ +# +# 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'; + +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]"); + } +}); + +no_long_string(); +no_root_location(); +log_level("info"); +run_tests; + +__DATA__ + +=== TEST 1: custom priority and default priority on different routes +--- 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": { + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + }, + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-post-function": { + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + }, + "serverless-pre-function": { + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello1" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: verify order +--- request +GET /hello +--- response_body +serverless-post-function +serverless-pre-function + + + +=== TEST 3: routing without custom plugin order is not affected +--- request +GET /hello1 +--- response_body +serverless-pre-function +serverless-post-function + + + +=== TEST 4: custom priority and default priority on same route +# the priority of serverless-post-function is -2000, execute serverless-post-function first +--- 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": { + "serverless-post-function": { + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + }, + "serverless-pre-function": { + "_meta": { + "priority": -2001 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: verify order +--- request +GET /hello +--- response_body +serverless-post-function +serverless-pre-function + + + +=== TEST 6: merge plugins from consumer and route, execute the rewrite phase +# in the rewrite phase, the plugins on the route must be executed first, +# and then executed the rewrite phase of the plugins on the consumer, +# and the custom plugin order fails for this case. +--- 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": { + "key-auth": { + "key": "auth-one" + }, + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {}, + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 7: verify order(more requests) +--- 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 httpc = http.new() + local headers = {} + headers["apikey"] = "auth-one" + local res, err = httpc:request_uri(uri, {method = "GET", headers = headers}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + + local res, err = httpc:request_uri(uri, {method = "GET", headers = headers}) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + } + } +--- response_body +serverless-pre-function +serverless-post-function +serverless-pre-function +serverless-post-function + + + +=== TEST 8: merge plugins form custom and route, execute the access phase +--- 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": { + "key-auth": { + "key": "auth-one" + }, + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "access", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "key-auth": {}, + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "access", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: verify order +--- request +GET /hello +--- more_headers +apikey: auth-one +--- response_body +serverless-post-function +serverless-pre-function + + + +=== TEST 10: merge plugins form service and route +--- 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": { + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "service_id": "1", + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 11: verify order +--- request +GET /hello +--- response_body +serverless-post-function +serverless-pre-function + + + +=== TEST 12: custom plugins sort is not affected by plugins reload +--- 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 httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + + local t = require("lib.test_admin").test + local code, _, org_body = t('/apisix/admin/plugins/reload', + ngx.HTTP_PUT) + + ngx.say(org_body) + + ngx.sleep(0.2) + + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + ngx.print(res.body) + } + } +--- response_body +serverless-post-function +serverless-pre-function +done +serverless-post-function +serverless-pre-function + + + +=== TEST 13: merge plugins form plugin_configs and route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, err = t('/apisix/admin/plugin_configs/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function\"); + end"] + } + } + }]] + ) + if code > 300 then + ngx.status = code + ngx.say(body) + end + + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function\"); + end"] + } + }, + "plugin_config_id": 1, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 14: verify order +--- request +GET /hello +--- response_body +serverless-post-function +serverless-pre-function + + + +=== TEST 15: custom plugins sort on 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_PUT, + [[{ + "plugins": { + "serverless-post-function": { + "_meta": { + "priority": 10000 + }, + "phase": "rewrite", + "functions" : ["return function(conf, ctx) + ngx.say(\"serverless-post-function on global rule\"); + end"] + }, + "serverless-pre-function": { + "_meta": { + "priority": -2000 + }, + "phase": "rewrite", + "functions": ["return function(conf, ctx) + ngx.say(\"serverless-pre-function on global rule\"); + end"] + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 16: verify order +--- request +GET /hello +--- response_body +serverless-post-function on global rule +serverless-pre-function on global rule +serverless-post-function +serverless-pre-function + + + +=== TEST 17: 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 + ngx.say(body) + end + ngx.say(body) + } + } +--- response_body +passed From 612e3775ec60111b0af547c3749364726a4bc44e Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Thu, 23 Jun 2022 18:50:05 +0530 Subject: [PATCH 45/63] docs: update "Loggers" Plugins (#7247) --- docs/en/latest/plugins/clickhouse-logger.md | 122 +++++++++++--------- docs/en/latest/plugins/log-rotate.md | 75 ++++++------ docs/en/latest/plugins/syslog.md | 59 +++++----- 3 files changed, 137 insertions(+), 119 deletions(-) diff --git a/docs/en/latest/plugins/clickhouse-logger.md b/docs/en/latest/plugins/clickhouse-logger.md index 2f3fd17446f3..505a26cd3160 100644 --- a/docs/en/latest/plugins/clickhouse-logger.md +++ b/docs/en/latest/plugins/clickhouse-logger.md @@ -1,5 +1,11 @@ --- title: clickhouse-logger +keywords: + - APISIX + - API Gateway + - Plugin + - ClickHouse Logger +description: This document contains information about the Apache APISIX clickhouse-logger Plugin. --- + +This document describes how to implement service discovery with Nacos and Zookeeper on the APISIX Control Plane. + +## APISIX-Seed Architecture + +Apache APISIX has supported Data Plane service discovery in the early days, and now APISIX also supports Control Plane service discovery through the [APISIX-Seed](https://github.com/api7/apisix-seed) project. The following figure shows the APISIX-Seed architecture diagram. + +![control-plane-service-discovery](../../../assets/images/control-plane-service-discovery.png) + +The specific information represented by the figures in the figure is as follows: + +1. Register an upstream with APISIX and specify the service discovery type. APISIX-Seed will watch APISIX resource changes in etcd, filter discovery types, and obtain service names. +2. APISIX-Seed subscribes the specified service name to the service registry to obtain changes to the corresponding service. +3. After the client registers the service with the service registry, APISIX-Seed will obtain the new service information and write the updated service node into etcd; +4. When the corresponding resources in etcd change, APISIX worker will refresh the latest service node information to memory. + +:::note + +It should be noted that after the introduction of APISIX-Seed, if the service of the registry changes frequently, the data in etcd will also change frequently. So, it is best to set the `--auto-compaction` option when starting etcd to compress the history periodically to avoid etcd eventually exhaust its storage space. Please refer to [revisions](https://etcd.io/docs/v3.5/learning/api/#revisions). + +::: + +## Why APISIX-Seed + +- Network topology becomes simpler + + APISIX does not need to maintain a network connection with each registry, and only needs to pay attention to the configuration information in etcd. This will greatly simplify the network topology. + +- Total data volume about upstream service becomes smaller + + Due to the characteristics of the registry, APISIX may store the full amount of registry service data in the worker, such as consul_kv. By introducing APISIX-Seed, each process of APISIX will not need to additionally cache upstream service-related information. + +- Easier to manage + + Service discovery configuration needs to be configured once per APISIX instance. By introducing APISIX-Seed, Apache APISIX will be in different to the configuration changes of the service registry. + +## Supported service registry + +ZooKeeper and Nacos are currently supported, and more service registries will be supported in the future. For more information, please refer to: [APISIX Seed](https://github.com/api7/apisix-seed#apisix-seed-for-apache-apisix). + +- If you want to enable control plane ZooKeeper service discovery, please refer to: [ZooKeeper Deployment Tutorial](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md). + +- If you want to enable control plane Nacos service discovery, please refer to: [Nacos Deployment Tutorial](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md). diff --git a/docs/en/latest/discovery/zookeeper.md b/docs/en/latest/discovery/zookeeper.md deleted file mode 100644 index 3adf52dc9735..000000000000 --- a/docs/en/latest/discovery/zookeeper.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: zookeeper ---- - - - -## Service Discovery Via Zookeeper - -`Zookeeper` service discovery needs to rely on the [apisix-seed](https://github.com/api7/apisix-seed) project. - -### How `apisix-seed` Works - -![APISIX-SEED](../../../assets/images/apisix-seed.svg) - -`apisix-seed` completes data exchange by watching the changes of `etcd` and `zookeeper` at the same time. - -The process is as follows: - -1. `APISIX` registers an upstream and specifies the service discovery type as `zookeeper` to `etcd`. -2. `apisix-seed` watches the resource changes of `APISIX` in `etcd` and filters the discovery type and obtains the service name. -3. `apisix-seed` binds the service to the `etcd` resource and starts watching the service in zookeeper. -4. The client registers the service with `zookeeper`. -5. `apisix-seed` gets the service changes in `zookeeper`. -6. `apisix-seed` queries the bound `etcd` resource information through the service name, and writes the updated service node to `etcd`. -7. The `APISIX` worker watches `etcd` changes and refreshes the service node information to the memory. - -### Setting `apisix-seed` and Zookeeper - -The configuration steps are as follows: - -1. Start the Zookeeper service - -```bash -docker run -itd --rm --name=dev-zookeeper -p 2181:2181 zookeeper:3.7.0 -``` - -2. Download and compile the `apisix-seed` project. - -```bash -git clone https://github.com/api7/apisix-seed.git -cd apisix-seed -go build -``` - -3. Modify the `apisix-seed` configuration file, config path `conf/conf.yaml`. - -```bash -etcd: # APISIX ETCD Configure - host: - - "http://127.0.0.1:2379" - prefix: /apisix - timeout: 30 - -discovery: - zookeeper: # Zookeeper Service Discovery - hosts: - - "127.0.0.1:2181" # Zookeeper service address - prefix: /zookeeper - weight: 100 # default weight for node - timeout: 10 # default 10s -``` - -4. Start `apisix-seed` to monitor service changes - -```bash -./apisix-seed -``` - -### Setting `APISIX` Route and Upstream - -Set a route, the request path is `/zk/*`, the upstream uses zookeeper as service discovery, and the service name -is `APISIX-ZK`. - -```shell -curl http://127.0.0.1:9080/apisix/admin/routes/1 \ --H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' -{ - "uri": "/zk/*", - "upstream": { - "service_name": "APISIX-ZK", - "type": "roundrobin", - "discovery_type": "zookeeper" - } -}' -``` - -### Register Service and verify Request - -1. Service registration using Zookeeper CLI - -- Register Service - -```bash -# Login Container -docker exec -it ${CONTAINERID} /bin/bash -# Login Zookeeper Client -oot@ae2f093337c1:/apache-zookeeper-3.7.0-bin# ./bin/zkCli.sh -# Register Service -[zk: localhost:2181(CONNECTED) 0] create /zookeeper/APISIX-ZK '{"host":"127.0.0.1:1980","weight":100}' -``` - -- Successful Response - -```bash -Created /zookeeper/APISIX-ZK -``` - -2. Verify Request - -- Request - -```bash -curl -i http://127.0.0.1:9080/zk/hello -``` - -- Response - -```bash -HTTP/1.1 200 OK -Connection: keep-alive -Content-Type: text/html; charset=utf-8 -Date: Tue, 29 Mar 2022 08:51:28 GMT -Server: APISIX/2.12.0 -Transfer-Encoding: chunked - -hello -``` diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 92b2967f3ad7..1cb779f484fe 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -206,7 +206,7 @@ "discovery/dns", "discovery/nacos", "discovery/eureka", - "discovery/zookeeper", + "discovery/control-plane-service-discovery", "discovery/kubernetes" ] }, diff --git a/docs/zh/latest/discovery/control-plane-service-discovery.md b/docs/zh/latest/discovery/control-plane-service-discovery.md new file mode 100644 index 000000000000..b6bcb7450901 --- /dev/null +++ b/docs/zh/latest/discovery/control-plane-service-discovery.md @@ -0,0 +1,72 @@ +--- +title: 控制面服务发现 +keywords: + - API 网关 + - APISIX + - ZooKeeper + - Nacos + - APISIX-Seed +description: 本文档介绍了如何在 API 网关 Apache APISIX 控制面通过 Nacos 和 Zookeeper 实现服务发现。 +--- + + + +本文档介绍了如何在 APISIX 控制面通过 Nacos 和 Zookeeper 实现服务发现。 + +## APISIX-Seed 架构 + +Apache APISIX 在早期已经支持了数据面服务发现,现在 APISIX 也通过 [APISIX-Seed](https://github.com/api7/apisix-seed) 项目实现了控制面服务发现,下图为 APISIX-Seed 架构图。 + +![control-plane-service-discovery](../../../assets/images/control-plane-service-discovery.png) + +图中的数字代表的具体信息如下: + +1. 通过 Admin API 向 APISIX 注册上游并指定服务发现类型。APISIX-Seed 将监听 etcd 中的 APISIX 资源变化,过滤服务发现类型并获取服务名称(如 ZooKeeper); +2. APISIX-Seed 将在服务注册中心(如 ZooKeeper)订阅指定的服务名称,以监控和更新对应的服务信息; +3. 客户端向服务注册中心注册服务后,APISIX-Seed 会获取新的服务信息,并将更新后的服务节点写入 etcd; +4. 当 APISIX-Seed 在 etcd 中更新相应的服务节点信息时,APISIX 会将最新的服务节点信息同步到内存中。 + +:::note + +引入 APISIX-Seed 后,如果注册中心的服务变化频繁,etcd 中的数据也会频繁变化。因此,需要在启动 etcd 时设置 `--auto-compaction` 选项,用来定期压缩历史记录,避免耗尽 etcd 存储空间。详细信息请参考 [revisions](https://etcd.io/docs/v3.5/learning/api/#revisions)。 + +::: + +## 为什么需要 APISIX-Seed? + +- 网络拓扑变得更简单 + + APISIX 不需要与每个注册中心保持网络连接,只需要关注 etcd 中的配置信息即可。这将大大简化网络拓扑。 + +- 上游服务总数据量变小 + + 由于 `registry` 的特性,APISIX 可能会在 Worker 中存储全量的 `registry` 服务数据,例如 Consul_KV。通过引入 APISIX-Seed,APISIX 的每个进程将不需要额外缓存上游服务相关信息。 + +- 更容易管理 + + 服务发现配置需要为每个 APISIX 实例配置一次。通过引入 APISIX-Seed,APISIX 将对服务注册中心的配置变化无感知。 + +## 支持的服务发现类型 + +目前已经支持了 ZooKeeper 和 Nacos,后续还将支持更多的服务注册中心,更多信息请参考:[APISIX Seed](https://github.com/api7/apisix-seed#apisix-seed-for-apache-apisix)。 + +- 如果你想启用控制面 ZooKeeper 服务发现,请参考:[ZooKeeper 部署教程](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md)。 + +- 如果你想启用控制面 Nacos 服务发现,请参考:[Nacos 部署教程](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md)。 diff --git a/docs/zh/latest/discovery/zookeeper.md b/docs/zh/latest/discovery/zookeeper.md deleted file mode 100644 index db2bec30103c..000000000000 --- a/docs/zh/latest/discovery/zookeeper.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: zookeeper -keywords: - - APISIX - - ZooKeeper - - apisix-seed -description: 本篇文档介绍了如何使用 ZooKeeper 做服务发现 ---- - - - -目前,如果你想在 APISIX 控制面使用 ZooKeeper 实现服务发现功能,需要依赖 [apisix-seed](https://github.com/api7/apisix-seed) 项目。 - -## `apisix-seed` 工作原理 - -![APISIX-SEED](../../../assets/images/apisix-seed.svg) - -`apisix-seed` 通过同时监听 etcd 和 ZooKeeper 的变化来完成数据交换。 - -流程如下: - -1. 使用 APISIX 注册一个上游服务,并将服务类型设置为 `zookeeper` 并保存到 etcd; -2. `apisix-seed` 监听 etcd 中 APISIX 的资源变更,并过滤服务发现类型获得服务名称; -3. `apisix-seed` 将服务绑定到 etcd 资源,并开始在 ZooKeeper 中监控此服务; -4. 客户端向 ZooKeeper 注册该服务; -5. `apisix-seed` 获取 ZooKeeper 中的服务变更; -6. `apisix-seed` 通过服务名称查询绑定的 etcd 资源,并将更新后的服务节点写入 etcd; -7. APISIX Worker 监控 etcd 资源变更,并在内存中刷新服务节点信息。 - -## 如何使用 - -### 环境准备:配置 `apisix-seed` 和 ZooKeeper - -1. 启动 ZooKeeper - -```bash -docker run -itd --rm --name=dev-zookeeper -p 2181:2181 zookeeper:3.7.0 -``` - -2. 下载并编译 `apisix-seed` 项目 - -```bash -git clone https://github.com/api7/apisix-seed.git -cd apisix-seed -go build -``` - -3. 参考以下信息修改 `apisix-seed` 配置文件,路径为 `conf/conf.yaml` - -```bash -etcd: # APISIX etcd 配置 - host: - - "http://127.0.0.1:2379" - prefix: /apisix - timeout: 30 - -discovery: - zookeeper: # 配置 ZooKeeper 进行服务发现 - hosts: - - "127.0.0.1:2181" # ZooKeeper 服务器地址 - prefix: /zookeeper - weight: 100 # ZooKeeper 节点默认权重设为 100 - timeout: 10 # ZooKeeper 会话超时时间默认设为 10 秒 -``` - -4. 启动 `apisix-seed` 以监听服务变更 - -```bash -./apisix-seed -``` - -### 设置 APISIX 路由和上游 - -通过以下命令设置路由,请求路径设置为 `/zk/*`,上游使用 ZooKeeper 作为服务发现,服务名称为 `APISIX-ZK`。 - -```shell -curl http://127.0.0.1:9080/apisix/admin/routes/1 \ --H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d ' -{ - "uri": "/zk/*", - "upstream": { - "service_name": "APISIX-ZK", - "type": "roundrobin", - "discovery_type": "zookeeper" - } -}' -``` - -### 注册服务 - -使用 ZooKeeper-cli 注册服务 - -登录 ZooKeeper 容器,使用 CLI 程序进行服务注册。具体命令如下: - -```bash -# 登陆容器 -docker exec -it ${CONTAINERID} /bin/bash -# 登陆 ZooKeeper 客户端 -oot@ae2f093337c1:/apache-zookeeper-3.7.0-bin# ./bin/zkCli.sh -# 注册服务 -[zk: localhost:2181(CONNECTED) 0] create /zookeeper/APISIX-ZK '{"host":"127.0.0.1","port":1980,"weight":100}' -``` - -返回结果如下: - -```bash -Created /zookeeper/APISIX-ZK -``` - -### 请求验证 - -通过以下命令请求路由: - -```bash -curl -i http://127.0.0.1:9080/zk/hello -``` - -正常返回结果: - -```bash -HTTP/1.1 200 OK -Connection: keep-alive -... -hello -``` From b7d1c93c533b9fa0a65cccf6a325a740d6380f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 27 Jun 2022 09:32:27 +0800 Subject: [PATCH 49/63] feat(sls-logger): support custom log format (#7328) Fix #7129 Signed-off-by: spacewander --- apisix/plugins/sls-logger.lua | 16 +++++++--- docs/en/latest/plugins/sls-logger.md | 34 ++++++++++++++++++++ docs/zh/latest/plugins/sls-logger.md | 26 ++++++++++++++++ t/plugin/sls-logger.t | 46 ++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/apisix/plugins/sls-logger.lua b/apisix/plugins/sls-logger.lua index ed34c847ebe2..290bf11917bb 100644 --- a/apisix/plugins/sls-logger.lua +++ b/apisix/plugins/sls-logger.lua @@ -17,6 +17,9 @@ local core = require("apisix.core") local log_util = require("apisix.utils.log-util") local bp_manager_mod = require("apisix.utils.batch-processor-manager") +local plugin = require("apisix.plugin") + + local plugin_name = "sls-logger" local ngx = ngx local rf5424 = require("apisix.plugins.slslog.rfc5424") @@ -127,10 +130,15 @@ end -- log phase in APISIX function _M.log(conf, ctx) - local entry = log_util.get_full_log(ngx, conf) - if not entry.route_id then - core.log.error("failed to obtain the route id for sys logger") - return + local metadata = plugin.plugin_metadata(plugin_name) + local entry + + 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) + else + entry = log_util.get_full_log(ngx, conf) end local json_str, err = core.json.encode(entry) diff --git a/docs/en/latest/plugins/sls-logger.md b/docs/en/latest/plugins/sls-logger.md index 1762f9271f33..e7be64606cd8 100644 --- a/docs/en/latest/plugins/sls-logger.md +++ b/docs/en/latest/plugins/sls-logger.md @@ -49,6 +49,40 @@ It might take some time to receive the log data. It will be automatically sent a This Plugin supports using batch processors to aggregate and process entries (logs/data) in a batch. This avoids the need for frequently submitting the data. The batch processor submits data every `5` seconds or when the data in the queue reaches `1000`. See [Batch Processor](../batch-processor.md#configuration) for more information or setting your custom configuration. +## Metadata + +You can also set the format of the logs by configuring the Plugin metadata. The following configurations are available: + +| Name | Type | Required | Default | Description | +| ---------- | ------ | -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| log_format | object | False | {"host": "$host", "@timestamp": "$time_iso8601", "client_ip": "$remote_addr"} | Log format declared as key value pairs in JSON format. Values only support strings. [APISIX](../apisix-variable.md) or [Nginx](http://nginx.org/en/docs/varindex.html) variables can be used by prefixing the string with `$`. | + +:::info IMPORTANT + +Configuring the Plugin metadata is global in scope. This means that it will take effect on all Routes and Services which use the `sls-logger` Plugin. + +::: + +The example below shows how you can configure through the Admin API: + +```shell +curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/sls-logger -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "log_format": { + "host": "$host", + "@timestamp": "$time_iso8601", + "client_ip": "$remote_addr" + } +}' +``` + +With this configuration, your logs would be formatted as shown below: + +```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"} +``` + ## Enabling the Plugin The example below shows how you can configure the Plugin on a specific Route: diff --git a/docs/zh/latest/plugins/sls-logger.md b/docs/zh/latest/plugins/sls-logger.md index 3ac708f9443a..d89914b06a26 100644 --- a/docs/zh/latest/plugins/sls-logger.md +++ b/docs/zh/latest/plugins/sls-logger.md @@ -46,6 +46,32 @@ title: sls-logger 本插件支持使用批处理器来聚合并批量处理条目(日志/数据)。这样可以避免插件频繁地提交数据,默认设置情况下批处理器会每 `5` 秒钟或队列中的数据达到 `1000` 条时提交数据,如需了解或自定义批处理器相关参数设置,请参考 [Batch-Processor](../batch-processor.md#配置) 配置部分。 +## 插件元数据设置 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ | +| 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 后,将对所有绑定 sls-logger 的 Route 或 Service 生效。 | + +### 设置日志格式示例 + +```shell +curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/sls-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"} +``` + ## 如何开启 1. 下面例子展示了如何为指定路由开启 `sls-logger` 插件的。 diff --git a/t/plugin/sls-logger.t b/t/plugin/sls-logger.t index 11db664cd22b..1c36383fb3b1 100644 --- a/t/plugin/sls-logger.t +++ b/t/plugin/sls-logger.t @@ -198,3 +198,49 @@ hello world --- response_body passed --- timeout: 5 + + + +=== TEST 8: add log format +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/plugin_metadata/sls-logger', + ngx.HTTP_PUT, + [[{ + "log_format": { + "host": "$host", + "client_ip": "$remote_addr" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 9: access +--- extra_init_by_lua + local json = require("toolkit.json") + local rfc5424 = require("apisix.plugins.slslog.rfc5424") + local old_f = rfc5424.encode + rfc5424.encode = function(facility, severity, hostname, appname, pid, project, + logstore, access_key_id, access_key_secret, msg) + local r = json.decode(msg) + assert(r.client_ip == "127.0.0.1", r.client_ip) + assert(r.host == "localhost", r.host) + return old_f(facility, severity, hostname, appname, pid, project, + logstore, access_key_id, access_key_secret, msg) + end +--- request +GET /hello +--- response_body +hello world From 55afd7d767effb62a9e988d4b1ac52b6c3cbeb6e Mon Sep 17 00:00:00 2001 From: fesily Date: Mon, 27 Jun 2022 09:32:47 +0800 Subject: [PATCH 50/63] feat: config center will check plugin_metadata (#7315) --- apisix/plugin.lua | 75 ++++++++++++++++---------- t/config-center-yaml/plugin-metadata.t | 24 ++++++++- 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index b0344eb0969e..d8f4d538c83d 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -41,7 +41,7 @@ local merged_route = core.lrucache.new({ ttl = 300, count = 512 }) local local_conf - +local check_plugin_metadata local _M = { version = 0.3, @@ -635,7 +635,10 @@ function _M.init_worker() end local plugin_metadatas, err = core.config.new("/plugin_metadata", - {automatic = true} + { + automatic = true, + checker = check_plugin_metadata + } ) if not plugin_metadatas then error("failed to create etcd instance for fetching /plugin_metadatas : " @@ -689,39 +692,55 @@ function _M.conf_version(conf) end -local function check_schema(plugins_conf, schema_type, skip_disabled_plugin) - for name, plugin_conf in pairs(plugins_conf) do - core.log.info("check plugin schema, name: ", name, ", configurations: ", - core.json.delay_encode(plugin_conf, true)) - if type(plugin_conf) ~= "table" then - return false, "invalid plugin conf " .. - core.json.encode(plugin_conf, true) .. - " for plugin [" .. name .. "]" +local function check_single_plugin_schema(name, plugin_conf, schema_type, skip_disabled_plugin) + core.log.info("check plugin schema, name: ", name, ", configurations: ", + core.json.delay_encode(plugin_conf, true)) + if type(plugin_conf) ~= "table" then + return false, "invalid plugin conf " .. + core.json.encode(plugin_conf, true) .. + " for plugin [" .. name .. "]" + end + + local plugin_obj = local_plugins_hash[name] + if not plugin_obj then + if skip_disabled_plugin then + return true + else + return false, "unknown plugin [" .. name .. "]" end + end - local plugin_obj = local_plugins_hash[name] - if not plugin_obj then - if skip_disabled_plugin then - goto CONTINUE - else - return false, "unknown plugin [" .. name .. "]" - end + if plugin_obj.check_schema then + local disable = plugin_conf.disable + plugin_conf.disable = nil + + local ok, err = plugin_obj.check_schema(plugin_conf, schema_type) + if not ok then + return false, "failed to check the configuration of plugin " + .. name .. " err: " .. err end - if plugin_obj.check_schema then - local disable = plugin_conf.disable - plugin_conf.disable = nil + plugin_conf.disable = disable + end - local ok, err = plugin_obj.check_schema(plugin_conf, schema_type) - if not ok then - return false, "failed to check the configuration of plugin " - .. name .. " err: " .. err - end + return true +end - plugin_conf.disable = disable - end - ::CONTINUE:: +check_plugin_metadata = function(item) + return check_single_plugin_schema(item.id, item, + core.schema.TYPE_METADATA, true) +end + + + +local function check_schema(plugins_conf, schema_type, skip_disabled_plugin) + for name, plugin_conf in pairs(plugins_conf) do + local ok, err = check_single_plugin_schema(name, plugin_conf, + schema_type, skip_disabled_plugin) + if not ok then + return false, err + end end return true diff --git a/t/config-center-yaml/plugin-metadata.t b/t/config-center-yaml/plugin-metadata.t index 0ad0c6c088e4..6e0a9971e879 100644 --- a/t/config-center-yaml/plugin-metadata.t +++ b/t/config-center-yaml/plugin-metadata.t @@ -33,7 +33,7 @@ _EOC_ $block->set_value("yaml_config", $yaml_config); - if (!$block->no_error_log) { + if (!$block->no_error_log && !$block->error_log) { $block->set_value("no_error_log", "[error]"); } }); @@ -67,3 +67,25 @@ plugin_metadata: GET /hello --- error_log "remote_addr":"127.0.0.1" + + + +=== TEST 2: sanity +--- apisix_yaml +upstreams: + - id: 1 + nodes: + "127.0.0.1:1980": 1 + type: roundrobin +routes: + - + uri: /hello + upstream_id: 1 +plugin_metadata: + - id: authz-casbin + model: 123 +#END +--- request +GET /hello +--- error_log +failed to check item data of [plugin_metadata] From dd7ee6f3b2c9f28d3a1924d98b884af20384bb72 Mon Sep 17 00:00:00 2001 From: tzssangglass Date: Mon, 27 Jun 2022 10:43:25 +0800 Subject: [PATCH 51/63] chore: adjust etcd max_fails for the admin api and add comments (#7311) --- apisix/core/config_etcd.lua | 1 + apisix/core/etcd.lua | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua index 8736059f7bf5..183c52aac338 100644 --- a/apisix/core/config_etcd.lua +++ b/apisix/core/config_etcd.lua @@ -523,6 +523,7 @@ local function _automatic_fetch(premature, self) end if not (health_check.conf and health_check.conf.shm_name) then + -- used for worker processes to synchronize configuration local _, err = health_check.init({ shm_name = health_check_shm_name, fail_timeout = self.health_check_timeout, diff --git a/apisix/core/etcd.lua b/apisix/core/etcd.lua index 274b3a9d80e9..df26e04dc243 100644 --- a/apisix/core/etcd.lua +++ b/apisix/core/etcd.lua @@ -85,10 +85,12 @@ local function new() end end - -- enable etcd health check retry for curr worker + -- if an unhealthy etcd node is selected in a single admin read/write etcd operation, + -- the retry mechanism for health check can select another healthy etcd node + -- to complete the read/write etcd operation. if not health_check.conf then health_check.init({ - max_fails = #etcd_conf.http_host, + max_fails = 1, retry = true, }) end From f03d3c797cb4362ff2573524f4e4c9dad90bf438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Mon, 27 Jun 2022 11:38:29 +0800 Subject: [PATCH 52/63] feat(deployment): send the right Host & SNI (#7323) Signed-off-by: spacewander --- apisix/cli/snippet.lua | 9 ++ apisix/conf_server.lua | 18 +++- t/deployment/conf_server.t | 163 +++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 2 deletions(-) diff --git a/apisix/cli/snippet.lua b/apisix/cli/snippet.lua index 2ce4a6627f07..24fa7e915dbd 100644 --- a/apisix/cli/snippet.lua +++ b/apisix/cli/snippet.lua @@ -60,6 +60,8 @@ function _M.generate_conf_server(env, conf) listen unix:{* home *}/conf/config_listen.sock; access_log off; + set $upstream_host ''; + access_by_lua_block { local conf_server = require("apisix.conf_server") conf_server.access() @@ -69,6 +71,11 @@ function _M.generate_conf_server(env, conf) {% if enable_https then %} proxy_pass https://apisix_conf_backend; proxy_ssl_server_name on; + {% if sni then %} + proxy_ssl_name {* sni *}; + {% else %} + proxy_ssl_name $upstream_host; + {% end %} proxy_ssl_protocols TLSv1.2 TLSv1.3; {% else %} proxy_pass http://apisix_conf_backend; @@ -76,6 +83,7 @@ function _M.generate_conf_server(env, conf) proxy_http_version 1.1; proxy_set_header Connection ""; + proxy_set_header Host $upstream_host; } log_by_lua_block { @@ -85,6 +93,7 @@ function _M.generate_conf_server(env, conf) } ]]) return conf_render({ + sni = etcd.tls and etcd.tls.sni, enable_https = enable_https, home = env.apisix_home or ".", }) diff --git a/apisix/conf_server.lua b/apisix/conf_server.lua index 9c59b37957df..40cf2895158b 100644 --- a/apisix/conf_server.lua +++ b/apisix/conf_server.lua @@ -21,6 +21,7 @@ local balancer = require("ngx.balancer") local error = error local ipairs = ipairs local ngx = ngx +local ngx_var = ngx.var local _M = {} @@ -35,6 +36,7 @@ local function create_resolved_result(server) return { host = host, port = port, + server = server, } end @@ -117,7 +119,7 @@ local function resolve_servers(ctx) if #servers > 1 then local nodes = {} for _, res in ipairs(resolved_results) do - local s = res.host .. ":" .. res.port + local s = res.server nodes[s] = 1 end server_picker = picker.new(nodes, {}) @@ -136,13 +138,25 @@ local function pick_node(ctx) ctx.server_picker = server_picker ctx.balancer_server = server - res = create_resolved_result(server) + + for _, r in ipairs(resolved_results) do + if r.server == server then + res = r + break + end + end else res = resolved_results[1] end ctx.balancer_ip = res.host ctx.balancer_port = res.port + + ngx_var.upstream_host = res.domain or res.host + if balancer.recreate_request and ngx.get_phase() == "balancer" then + balancer.recreate_request() + end + return true end diff --git a/t/deployment/conf_server.t b/t/deployment/conf_server.t index 2e89ac2ae132..c6a088b380bb 100644 --- a/t/deployment/conf_server.t +++ b/t/deployment/conf_server.t @@ -264,3 +264,166 @@ deployment: connect() failed --- response_body foo + + + +=== TEST 6: check default SNI +--- http_config +server { + listen 12345 ssl; + ssl_certificate cert/apisix.crt; + ssl_certificate_key cert/apisix.key; + + ssl_certificate_by_lua_block { + local ngx_ssl = require "ngx.ssl" + ngx.log(ngx.WARN, "Receive SNI: ", ngx_ssl.server_name()) + } + + location / { + proxy_pass http://127.0.0.1:2379; + } +} +--- config + location /t { + content_by_lua_block { + local etcd = require("apisix.core.etcd") + assert(etcd.set("/apisix/test", "foo")) + local res = assert(etcd.get("/apisix/test")) + ngx.say(res.body.node.value) + } + } +--- response_body +foo +--- extra_yaml_config +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - https://localhost:12345 +--- error_log +Receive SNI: localhost +--- no_error_log +[error] + + + +=== TEST 7: check configured SNI +--- http_config +server { + listen 12345 ssl; + ssl_certificate cert/apisix.crt; + ssl_certificate_key cert/apisix.key; + + ssl_certificate_by_lua_block { + local ngx_ssl = require "ngx.ssl" + ngx.log(ngx.WARN, "Receive SNI: ", ngx_ssl.server_name()) + } + + location / { + proxy_pass http://127.0.0.1:2379; + } +} +--- config + location /t { + content_by_lua_block { + local etcd = require("apisix.core.etcd") + assert(etcd.set("/apisix/test", "foo")) + local res = assert(etcd.get("/apisix/test")) + ngx.say(res.body.node.value) + } + } +--- response_body +foo +--- extra_yaml_config +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - https://127.0.0.1:12345 + tls: + sni: "x.com" +--- error_log +Receive SNI: x.com +--- no_error_log +[error] + + + +=== TEST 8: check Host header +--- http_config +server { + listen 12345; + location / { + access_by_lua_block { + ngx.log(ngx.WARN, "Receive Host: ", ngx.var.http_host) + } + proxy_pass http://127.0.0.1:2379; + } +} +--- config + location /t { + content_by_lua_block { + local etcd = require("apisix.core.etcd") + assert(etcd.set("/apisix/test", "foo")) + local res = assert(etcd.get("/apisix/test")) + ngx.say(res.body.node.value) + } + } +--- response_body +foo +--- extra_yaml_config +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - http://127.0.0.1:12345 + - http://localhost:12345 +--- error_log +Receive Host: localhost +Receive Host: 127.0.0.1 + + + +=== TEST 9: check Host header after retry +--- http_config +server { + listen 12345; + location / { + access_by_lua_block { + ngx.log(ngx.WARN, "Receive Host: ", ngx.var.http_host) + } + proxy_pass http://127.0.0.1:2379; + } +} +--- config + location /t { + content_by_lua_block { + local etcd = require("apisix.core.etcd") + assert(etcd.set("/apisix/test", "foo")) + local res = assert(etcd.get("/apisix/test")) + ngx.say(res.body.node.value) + } + } +--- response_body +foo +--- extra_yaml_config +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + prefix: "/apisix" + host: + - http://127.0.0.1:1979 + - http://localhost:12345 +--- error_log +Receive Host: localhost From 436279c5ac73a8a755ba2b61c1dacfcb163d4ee8 Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Mon, 27 Jun 2022 11:21:57 +0530 Subject: [PATCH 53/63] docs: update "Loggers" Plugins 5/5 (#7308) --- docs/en/latest/plugins/file-logger.md | 88 +++++++++--------- docs/en/latest/plugins/loggly.md | 97 +++++++++++--------- docs/en/latest/plugins/splunk-hec-logging.md | 54 ++++++----- 3 files changed, 129 insertions(+), 110 deletions(-) diff --git a/docs/en/latest/plugins/file-logger.md b/docs/en/latest/plugins/file-logger.md index 27bc93d69089..8ad5cc1dea0c 100644 --- a/docs/en/latest/plugins/file-logger.md +++ b/docs/en/latest/plugins/file-logger.md @@ -1,5 +1,11 @@ --- title: file-logger +keywords: + - APISIX + - API Gateway + - Plugin + - File Logger +description: This document contains information about the Apache APISIX file-logger Plugin. --- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +如果你希望为 APISIX 做出贡献或配置开发环境,你可以参考本教程。 + +如果你想通过其他方式安装 APISIX,你可以参考[安装指南](./installation-guide.md)。 + +:::note + +如果你想为特定的环境或打包 APISIX,请参考 [apisix-build-tools](https://github.com/api7/apisix-build-tools)。 + +::: + +## 源码安装 APISIX + +首先,你可以通过以下命令安装依赖项: + +```shell +curl https://raw.githubusercontent.com/apache/apisix/master/utils/install-dependencies.sh -sL | bash - +``` + +然后,创建一个目录并设置环境变量 `APISIX_VERSION`: + +```shell +APISIX_VERSION='2.14.1' +mkdir apisix-${APISIX_VERSION} +``` + +现在,你可以运行以下命令来下载 APISIX 源码包: + +```shell +wget https://downloads.apache.org/apisix/${APISIX_VERSION}/apache-apisix-${APISIX_VERSION}-src.tgz +``` + +你可以从[下载页面](https://apisix.apache.org/downloads/)下载源码包。你也可以在该页面找到 APISIX Dashboard 和 APISIX Ingress Controller 的源码包。 + +下载源码包后,你可以将文件解压到之前创建的文件夹中: + +```shell +tar zxvf apache-apisix-${APISIX_VERSION}-src.tgz -C apisix-${APISIX_VERSION} +``` + +然后切换到解压的目录,创建依赖项并安装 APISIX,如下所示: + +```shell +cd apisix-${APISIX_VERSION} +make deps +make install +``` + +该命令将安装 APISIX 运行时依赖的 Lua 库和 `apisix` 命令。 + +:::note + +如果你在运行 `make deps` 时收到类似 `Could not find header file for LDAP/PCRE/openssl` 的错误消息,请使用此解决方案。 + +`luarocks` 支持自定义编译时依赖项(请参考:[配置文件格式](https://github.com/luarocks/luarocks/wiki/Config-file-format))。你可以使用第三方工具安装缺少的软件包并将其安装目录添加到 `luarocks` 变量表中。此方法适用于 macOS、Ubuntu、CentOS 和其他类似操作系统。 + +此处仅给出 macOS 的具体解决步骤,其他操作系统的解决方案类似: + +1. 安装 `openldap`: + + ```shell + brew install openldap + ``` + +2. 使用以下命令命令找到本地安装目录: + + ```shell + brew --prefix openldap + ``` + +3. 将路径添加到项目配置文件中(选择两种方法中的一种即可): + 1. 你可以使用 `luarocks config` 命令设置 `LDAP_DIR`: + + ```shell + luarocks config variables.LDAP_DIR /opt/homebrew/cellar/openldap/2.6.1 + ``` + + 2. 你还可以更改 `luarocks` 的默认配置文件。打开 `~/.luaorcks/config-5.1.lua` 文件并添加以下内容: + + ```shell + variables = { LDAP_DIR = "/opt/homebrew/cellar/openldap/2.6.1", LDAP_INCDIR = "/opt/homebrew/cellar/openldap/2.6.1/include", } + ``` + + `/opt/homebrew/cellar/openldap/` 是 `brew` 在 macOS(Apple Silicon) 上安装 `openldap` 的默认位置。`/usr/local/opt/openldap/` 是 brew 在 macOS(Intel) 上安装 openldap 的默认位置。 + +::: + +如果你不再需要 APISIX,可以执行以下命令卸载: + +```shell +make uninstall && make undeps +``` + +:::danger + +该操作将删除所有相关文件。 + +::: + +## 安装 etcd + +APISIX 默认使用 [etcd](https://github.com/etcd-io/etcd) 来保存和同步配置。在运行 APISIX 之前,你需要在你的机器上安装 etcd。 + + + + +```shell +ETCD_VERSION='3.4.18' +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 >/tmp/etcd.log 2>&1 & +``` + + + + + +```shell +brew install etcd +brew services start etcd +``` + + + + +## 管理 APISIX 服务 + +运行以下命令初始化 NGINX 配置文件和 etcd。 + +```shell +apisix init +``` + +:::tip + +你可以运行 `apisix help` 命令,查看返回结果,获取其他操作命令及其描述。 + +::: + +运行以下命令测试配置文件,APISIX 将根据 `config.yaml` 生成 `nginx.conf`,并检查 `nginx.conf` 的语法是否正确。 + +```shell +apisix test +``` + +最后,你可以使用以下命令运行 APISIX。 + +```shell +apisix start +``` + +如果需要停止 APISIX,你可以使用 `apisix quit` 或者 `apisix stop` 命令。 + +`apisix quit` 将正常关闭 APISIX,该指令确保在停止之前完成所有收到的请求。 + +```shell +apisix quit +``` + +`apisix stop` 命令会强制关闭 APISIX 并丢弃所有请求。 + +```shell +apisix stop +``` + +## 为 APISIX 构建 APISIX-Base + +APISIX 的一些特性需要在 OpenResty 中引入额外的 NGINX 模块。 + +如果要使用这些功能,你需要构建一个自定义的 OpenResty 发行版(APISIX-Base)。请参考 [apisix-build-tools](https://github.com/api7/apisix-build-tools) 配置你的构建环境并进行构建。 + +## 运行测试用例 + +以下步骤展示了如何运行 APISIX 的测试用例: + +1. 安装 `perl` 的包管理器 [cpanminus](https://metacpan.org/pod/App::cpanminus#INSTALLATION)。 +2. 通过 `cpanm` 来安装 [test-nginx](https://github.com/openresty/test-nginx) 的依赖: + + ```shell + sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1) + ``` + +3. 将 `test-nginx` 源码克隆到本地: + + ```shell + git clone https://github.com/openresty/test-nginx.git + ``` + +4. 运行以下命令将当前目录添加到 Perl 的模块目录: + + ```shell + export PERL5LIB=.:$PERL5LIB + ``` + + 你可以通过运行以下命令指定 NGINX 二进制路径: + + ```shell + TEST_NGINX_BINARY=/usr/local/bin/openresty prove -Itest-nginx/lib -r t + ``` + +5. 运行测试: + + ```shell + make test + ``` + +:::note + +部分测试需要依赖外部服务和修改系统配置。如果想要完整地构建测试环境,请参考 [ci/linux_openresty_common_runner.sh](https://github.com/apache/apisix/blob/master/ci/linux_openresty_common_runner.sh)。 + +::: + +### 故障排查 + +以下是运行 APISIX 测试用例的常见故障排除步骤。 + +出现 `Error unknown directive "lua_package_path" in /API_ASPIX/apisix/t/servroot/conf/nginx.conf` 报错,是因为默认的 NGINX 安装路径未找到,解决方法如下: + +- Linux 默认安装路径: + + ```shell + export PATH=/usr/local/openresty/nginx/sbin:$PATH + ``` + +- macOS 通过 `homebrew` 的默认安装路径: + + ```shell + export PATH=/usr/local/opt/openresty/nginx/sbin:$PATH + ``` + +### 运行指定的测试用例 + +使用以下命令运行指定的测试用例: + +```shell +prove -Itest-nginx/lib -r t/plugin/openid-connect.t +``` + +如果你想要了解更多信息,请参考 [testing framework](https://github.com/apache/apisix/blob/master/docs/en/latest/internal/testing-framework.md)。 diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 1cb779f484fe..0832c0429924 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -190,6 +190,16 @@ } ] }, + { + "type": "category", + "label": "Development", + "items": [ + { + "type": "doc", + "id": "building-apisix" + } + ] + }, { "type": "doc", "id": "FAQ" From c781376ce1d7340ba090cbc1d0f956ad1da564d6 Mon Sep 17 00:00:00 2001 From: spacewander Date: Tue, 28 Jun 2022 09:32:31 +0800 Subject: [PATCH 59/63] ci: don't start unused service Signed-off-by: spacewander --- .github/workflows/build.yml | 62 ++++-- .github/workflows/centos7-ci.yml | 34 ++- Makefile | 1 - ci/init-last-test-service.sh | 30 +++ ...service.sh => init-plugin-test-service.sh} | 9 - ...r-compose.yml => docker-compose.first.yml} | 195 ----------------- ci/pod/docker-compose.last.yml | 97 +++++++++ ci/pod/docker-compose.plugin.yml | 201 ++++++++++++++++++ ci/pod/kafka/kafka-server/env/common.env | 9 +- ci/pod/kafka/kafka-server/env/last.env | 8 + 10 files changed, 414 insertions(+), 232 deletions(-) create mode 100755 ci/init-last-test-service.sh rename ci/{linux-ci-init-service.sh => init-plugin-test-service.sh} (85%) rename ci/pod/{docker-compose.yml => docker-compose.first.yml} (55%) create mode 100644 ci/pod/docker-compose.last.yml create mode 100644 ci/pod/docker-compose.plugin.yml create mode 100644 ci/pod/kafka/kafka-server/env/last.env diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6834d8651c18..21d185cbe455 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,6 +67,21 @@ jobs: echo "##[set-output name=version;]$(echo ${GITHUB_REF##*/})" echo "##[set-output name=fullname;]$(echo apache-apisix-${GITHUB_REF##*/}-src.tgz)" + - name: Extract test type + shell: bash + id: test_env + run: | + test_dir="${{ matrix.test_dir }}" + if [[ $test_dir =~ 't/plugin' ]]; then + echo "##[set-output name=type;]$(echo 'plugin')" + fi + if [[ $test_dir =~ 't/admin ' ]]; then + echo "##[set-output name=type;]$(echo 'first')" + fi + if [[ $test_dir =~ ' t/xrpc' ]]; then + echo "##[set-output name=type;]$(echo 'last')" + fi + - name: Linux launch common services run: | make ci-env-up project_compose_ci=ci/pod/docker-compose.common.yml @@ -82,32 +97,28 @@ jobs: rm -rf $(ls -1 --ignore=*.tgz --ignore=ci --ignore=t --ignore=utils --ignore=.github) tar zxvf ${{ steps.branch_env.outputs.fullname }} - - name: Build wasm code - if: matrix.os_name == 'linux_openresty' + - name: Start CI env (FIRST_TEST) + if: steps.test_env.outputs.type == 'first' 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 "*.go" | xargs -Ip tinygo build -o p.wasm -scheduler=none -target=wasi p + # launch deps env + make ci-env-up project_compose_ci=ci/pod/docker-compose.${{ steps.test_env.outputs.type }}.yml - - name: Build xDS library + - name: Start CI env (PLUGIN_TEST) + if: steps.test_env.outputs.type == 'plugin' run: | - cd t/xds-library - go build -o libxds.so -buildmode=c-shared main.go export.go + make ci-env-up project_compose_ci=ci/pod/docker-compose.${{ steps.test_env.outputs.type }}.yml + sudo ./ci/init-${{ steps.test_env.outputs.type }}-test-service.sh - - name: Linux Before install - run: sudo ./ci/${{ matrix.os_name }}_runner.sh before_install - - - name: Start CI env + - name: Start CI env (LAST_TEST) + if: steps.test_env.outputs.type == 'last' run: | # generating SSL certificates for Kafka sudo keytool -genkeypair -keyalg RSA -dname "CN=127.0.0.1" -alias 127.0.0.1 -keystore ./ci/pod/kafka/kafka-server/selfsigned.jks -validity 365 -keysize 2048 -storepass changeit - # launch deps env - make ci-env-up - sudo ./ci/linux-ci-init-service.sh + make ci-env-up project_compose_ci=ci/pod/docker-compose.${{ steps.test_env.outputs.type }}.yml + sudo ./ci/init-${{ steps.test_env.outputs.type }}-test-service.sh - name: Start Dubbo Backend - if: matrix.os_name == 'linux_openresty' + if: matrix.os_name == 'linux_openresty' && steps.test_env.outputs.type == 'plugin' run: | sudo apt install -y maven cd t/lib/dubbo-backend @@ -115,6 +126,23 @@ jobs: cd dubbo-backend-provider/target java -Djava.net.preferIPv4Stack=true -jar dubbo-demo-provider.one-jar.jar > /tmp/java.log & + - name: Build xDS library + if: steps.test_env.outputs.type == 'last' + run: | + cd t/xds-library + go build -o libxds.so -buildmode=c-shared main.go export.go + + - name: Build wasm code + if: matrix.os_name == 'linux_openresty' && steps.test_env.outputs.type == 'last' + 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 "*.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 + - name: Linux Install run: | sudo --preserve-env=OPENRESTY_VERSION \ diff --git a/.github/workflows/centos7-ci.yml b/.github/workflows/centos7-ci.yml index 589e5ed69225..b308c79fb95b 100644 --- a/.github/workflows/centos7-ci.yml +++ b/.github/workflows/centos7-ci.yml @@ -45,6 +45,21 @@ jobs: run: | echo "##[set-output name=version;]$(echo ${GITHUB_REF##*/})" + - name: Extract test type + shell: bash + id: test_env + run: | + test_dir="${{ matrix.test_dir }}" + if [[ $test_dir =~ 't/plugin' ]]; then + echo "##[set-output name=type;]$(echo 'plugin')" + fi + if [[ $test_dir =~ 't/admin ' ]]; then + echo "##[set-output name=type;]$(echo 'first')" + fi + if [[ $test_dir =~ ' t/xds-library' ]]; then + echo "##[set-output name=type;]$(echo 'last')" + fi + - name: Linux launch common services run: | make ci-env-up project_compose_ci=ci/pod/docker-compose.common.yml @@ -66,6 +81,7 @@ jobs: rm -rf $(ls -1 --ignore=apisix-build-tools --ignore=t --ignore=utils --ignore=ci --ignore=Makefile --ignore=rockspec) - name: Build xDS library + if: steps.test_env.outputs.type == 'last' run: | cd t/xds-library go build -o libxds.so -buildmode=c-shared main.go export.go @@ -77,12 +93,24 @@ jobs: docker run -itd -v /home/runner/work/apisix/apisix:/apisix --env TEST_FILE_SUB_DIR="$TEST_FILE_SUB_DIR" --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 + - name: Start CI env (FIRST_TEST) + if: steps.test_env.outputs.type == 'first' + run: | + make ci-env-up project_compose_ci=ci/pod/docker-compose.${{ steps.test_env.outputs.type }}.yml + + - name: Start CI env (PLUGIN_TEST) + if: steps.test_env.outputs.type == 'plugin' + run: | + make ci-env-up project_compose_ci=ci/pod/docker-compose.${{ steps.test_env.outputs.type }}.yml + ./ci/init-${{ steps.test_env.outputs.type }}-test-service.sh + + - name: Start CI env (LAST_TEST) + if: steps.test_env.outputs.type == 'last' run: | # generating SSL certificates for Kafka keytool -genkeypair -keyalg RSA -dname "CN=127.0.0.1" -alias 127.0.0.1 -keystore ./ci/pod/kafka/kafka-server/selfsigned.jks -validity 365 -keysize 2048 -storepass changeit - make ci-env-up - ./ci/linux-ci-init-service.sh + make ci-env-up project_compose_ci=ci/pod/docker-compose.${{ steps.test_env.outputs.type }}.yml + ./ci/init-${{ steps.test_env.outputs.type }}-test-service.sh - name: Install dependencies run: | diff --git a/Makefile b/Makefile index 989fe3714f8a..6c82a6a94341 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,6 @@ SHELL := /bin/bash -o pipefail # Project basic setting VERSION ?= master project_name ?= apache-apisix -project_compose_ci ?= ci/pod/docker-compose.yml project_release_name ?= $(project_name)-$(VERSION)-src diff --git a/ci/init-last-test-service.sh b/ci/init-last-test-service.sh new file mode 100755 index 000000000000..f49d4a747528 --- /dev/null +++ b/ci/init-last-test-service.sh @@ -0,0 +1,30 @@ +#!/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. +# + +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 +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 test-consumer + +# create messages for test-consumer +for i in `seq 30` +do + docker exec -i apache-apisix_kafka-server1_1 bash -c "echo "testmsg$i" | /opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic test-consumer" + echo "Produces messages to the test-consumer topic, msg: testmsg$i" +done +echo "Kafka service initialization completed" diff --git a/ci/linux-ci-init-service.sh b/ci/init-plugin-test-service.sh similarity index 85% rename from ci/linux-ci-init-service.sh rename to ci/init-plugin-test-service.sh index 73477a5febca..5f468502304d 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/init-plugin-test-service.sh @@ -19,15 +19,6 @@ 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 -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 test-consumer - -# create messages for test-consumer -for i in `seq 30` -do - docker exec -i apache-apisix_kafka-server1_1 bash -c "echo "testmsg$i" | /opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server 127.0.0.1:9092 --topic test-consumer" - echo "Produces messages to the test-consumer topic, msg: testmsg$i" -done -echo "Kafka service initialization completed" # prepare openwhisk env docker pull openwhisk/action-nodejs-v14:nightly diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.first.yml similarity index 55% rename from ci/pod/docker-compose.yml rename to ci/pod/docker-compose.first.yml index 68dab85c539b..a13ad3cf1586 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.first.yml @@ -18,95 +18,6 @@ version: "3.8" services: - ## Redis - apisix_redis: - # The latest image is the latest stable version - image: redis:latest - restart: unless-stopped - ports: - - "6379:6379" - networks: - apisix_net: - - - ## keycloak - apisix_keycloak: - image: sshniro/keycloak-apisix:1.0.0 - environment: - KEYCLOAK_USER: admin - KEYCLOAK_PASSWORD: 123456 - restart: unless-stopped - ports: - - "8090:8080" - - "8443:8443" - networks: - apisix_net: - - - ## kafka-cluster - zookeeper-server1: - image: bitnami/zookeeper:3.6.0 - env_file: - - ci/pod/kafka/zookeeper-server/env/common.env - restart: unless-stopped - ports: - - "2181:2181" - networks: - kafka_net: - - zookeeper-server2: - image: bitnami/zookeeper:3.6.0 - env_file: - - ci/pod/kafka/zookeeper-server/env/common.env - restart: unless-stopped - ports: - - "12181:12181" - networks: - kafka_net: - - kafka-server1: - image: bitnami/kafka:2.8.1 - env_file: - - ci/pod/kafka/kafka-server/env/common.env - environment: - KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server1:2181 - restart: unless-stopped - ports: - - "9092:9092" - - "9093:9093" - - "9094:9094" - depends_on: - - zookeeper-server1 - - zookeeper-server2 - networks: - kafka_net: - volumes: - - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro - - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro - - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro - - kafka-server2: - image: bitnami/kafka:2.8.1 - env_file: - - ci/pod/kafka/kafka-server/env/common.env - environment: - KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server2:2181 - restart: unless-stopped - ports: - - "19092:9092" - - "19093:9093" - - "19094:9094" - depends_on: - - zookeeper-server1 - - zookeeper-server2 - networks: - kafka_net: - volumes: - - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro - - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro - - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro - - ## Eureka eureka: image: bitinit/eureka @@ -116,19 +27,6 @@ services: ports: - "8761:8761" - - ## SkyWalking - skywalking: - image: apache/skywalking-oap-server:8.7.0-es6 - restart: unless-stopped - ports: - - "1234:1234" - - "11800:11800" - - "12800:12800" - networks: - skywalk_net: - - ## Consul consul_1: image: consul:1.7 @@ -148,37 +46,6 @@ services: networks: 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 - environment: - LDAP_ADMIN_USERNAME: amdin - LDAP_ADMIN_PASSWORD: adminpassword - LDAP_USERS: user01,user02 - LDAP_PASSWORDS: password1,password2 - ports: - - "1389:1389" - - "1636:1636" - - ## Nacos cluster nacos_auth: hostname: nacos1 @@ -368,69 +235,7 @@ services: networks: nacos_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: - - # Open Policy Agent - opa: - image: openpolicyagent/opa:0.35.0 - restart: unless-stopped - ports: - - 8181:8181 - 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 - 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: - kafka_net: nacos_net: - skywalk_net: - rocketmq_net: - vault_net: - opa_net: diff --git a/ci/pod/docker-compose.last.yml b/ci/pod/docker-compose.last.yml new file mode 100644 index 000000000000..dbc835fdeaf7 --- /dev/null +++ b/ci/pod/docker-compose.last.yml @@ -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. +# + +version: "3.8" + +services: + ## Redis + apisix_redis: + # The latest image is the latest stable version + image: redis:latest + restart: unless-stopped + ports: + - "6379:6379" + networks: + apisix_net: + + ## kafka-cluster + zookeeper-server1: + image: bitnami/zookeeper:3.6.0 + env_file: + - ci/pod/kafka/zookeeper-server/env/common.env + restart: unless-stopped + ports: + - "2181:2181" + networks: + kafka_net: + + zookeeper-server2: + image: bitnami/zookeeper:3.6.0 + env_file: + - ci/pod/kafka/zookeeper-server/env/common.env + restart: unless-stopped + ports: + - "12181:12181" + networks: + kafka_net: + + kafka-server1: + image: bitnami/kafka:2.8.1 + env_file: + - ci/pod/kafka/kafka-server/env/last.env + environment: + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server1:2181 + restart: unless-stopped + ports: + - "9092:9092" + - "9093:9093" + - "9094:9094" + depends_on: + - zookeeper-server1 + - zookeeper-server2 + networks: + kafka_net: + volumes: + - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro + - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro + - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro + + kafka-server2: + image: bitnami/kafka:2.8.1 + env_file: + - ci/pod/kafka/kafka-server/env/last.env + environment: + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server2:2181 + restart: unless-stopped + ports: + - "19092:9092" + - "19093:9093" + - "19094:9094" + depends_on: + - zookeeper-server1 + - zookeeper-server2 + networks: + kafka_net: + volumes: + - ./ci/pod/kafka/kafka-server/kafka_jaas.conf:/opt/bitnami/kafka/config/kafka_jaas.conf:ro + - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.keystore.jks:ro + - ./ci/pod/kafka/kafka-server/selfsigned.jks:/opt/bitnami/kafka/config/certs/kafka.truststore.jks:ro + + +networks: + apisix_net: + kafka_net: diff --git a/ci/pod/docker-compose.plugin.yml b/ci/pod/docker-compose.plugin.yml new file mode 100644 index 000000000000..d0350860096b --- /dev/null +++ b/ci/pod/docker-compose.plugin.yml @@ -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. +# + +version: "3.8" + +services: + ## Redis + apisix_redis: + # The latest image is the latest stable version + image: redis:latest + restart: unless-stopped + ports: + - "6379:6379" + networks: + apisix_net: + + + ## keycloak + apisix_keycloak: + image: sshniro/keycloak-apisix:1.0.0 + environment: + KEYCLOAK_USER: admin + KEYCLOAK_PASSWORD: 123456 + restart: unless-stopped + ports: + - "8090:8080" + - "8443:8443" + networks: + apisix_net: + + + ## kafka-cluster + zookeeper-server1: + image: bitnami/zookeeper:3.6.0 + env_file: + - ci/pod/kafka/zookeeper-server/env/common.env + restart: unless-stopped + ports: + - "2181:2181" + networks: + kafka_net: + + zookeeper-server2: + image: bitnami/zookeeper:3.6.0 + env_file: + - ci/pod/kafka/zookeeper-server/env/common.env + restart: unless-stopped + ports: + - "12181:12181" + networks: + kafka_net: + + kafka-server1: + image: bitnami/kafka:2.8.1 + env_file: + - ci/pod/kafka/kafka-server/env/common.env + environment: + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server1:2181 + restart: unless-stopped + ports: + - "9092:9092" + depends_on: + - zookeeper-server1 + - zookeeper-server2 + networks: + kafka_net: + + kafka-server2: + image: bitnami/kafka:2.8.1 + env_file: + - ci/pod/kafka/kafka-server/env/common.env + environment: + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper-server2:2181 + restart: unless-stopped + ports: + - "19092:9092" + depends_on: + - zookeeper-server1 + - zookeeper-server2 + networks: + kafka_net: + + ## SkyWalking + skywalking: + image: apache/skywalking-oap-server:8.7.0-es6 + restart: unless-stopped + ports: + - "1234:1234" + - "11800:11800" + - "12800:12800" + networks: + skywalk_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 + environment: + LDAP_ADMIN_USERNAME: amdin + LDAP_ADMIN_PASSWORD: adminpassword + LDAP_USERS: user01,user02 + LDAP_PASSWORDS: password1,password2 + ports: + - "1389:1389" + - "1636:1636" + + + 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: + + # Open Policy Agent + opa: + image: openpolicyagent/opa:0.35.0 + restart: unless-stopped + ports: + - 8181:8181 + 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 + 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: + kafka_net: + skywalk_net: + rocketmq_net: + vault_net: + opa_net: diff --git a/ci/pod/kafka/kafka-server/env/common.env b/ci/pod/kafka/kafka-server/env/common.env index adc9d7cad1f8..06200b9b0042 100644 --- a/ci/pod/kafka/kafka-server/env/common.env +++ b/ci/pod/kafka/kafka-server/env/common.env @@ -1,8 +1,3 @@ ALLOW_PLAINTEXT_LISTENER=yes -KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false -KAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9093,SASL_PLAINTEXT://0.0.0.0:9094 -KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9093,SASL_PLAINTEXT://127.0.0.1:9094 -KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM= -KAFKA_CFG_SSL_KEYSTORE_LOCATION=/opt/bitnami/kafka/config/certs/kafka.keystore.jks -KAFKA_CFG_SSL_KEYSTORE_PASSWORD=changeit -KAFKA_CFG_SSL_KEY_PASSWORD=changeit +KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true +KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 diff --git a/ci/pod/kafka/kafka-server/env/last.env b/ci/pod/kafka/kafka-server/env/last.env new file mode 100644 index 000000000000..adc9d7cad1f8 --- /dev/null +++ b/ci/pod/kafka/kafka-server/env/last.env @@ -0,0 +1,8 @@ +ALLOW_PLAINTEXT_LISTENER=yes +KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false +KAFKA_CFG_LISTENERS=PLAINTEXT://0.0.0.0:9092,SSL://0.0.0.0:9093,SASL_PLAINTEXT://0.0.0.0:9094 +KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092,SSL://127.0.0.1:9093,SASL_PLAINTEXT://127.0.0.1:9094 +KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM= +KAFKA_CFG_SSL_KEYSTORE_LOCATION=/opt/bitnami/kafka/config/certs/kafka.keystore.jks +KAFKA_CFG_SSL_KEYSTORE_PASSWORD=changeit +KAFKA_CFG_SSL_KEY_PASSWORD=changeit From c0a46e9a796a07f0521cffd1a9ea49a7448a2409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= Date: Thu, 30 Jun 2022 10:06:33 +0800 Subject: [PATCH 60/63] feat(deployment): support mTLS in traditional mode (#7331) Signed-off-by: spacewander --- apisix/cli/file.lua | 7 ++++ apisix/cli/snippet.lua | 16 +++++++++ t/cli/test_deployment_traditional.sh | 53 ++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua index 66600b54b41b..9c528005e1fd 100644 --- a/apisix/cli/file.lua +++ b/apisix/cli/file.lua @@ -251,6 +251,13 @@ function _M.read_yaml_conf(apisix_home) end end + if default_conf.deployment + and default_conf.deployment.role == "traditional" + and default_conf.deployment.etcd + then + default_conf.etcd = default_conf.deployment.etcd + end + return default_conf end diff --git a/apisix/cli/snippet.lua b/apisix/cli/snippet.lua index 24fa7e915dbd..bfaf973a026c 100644 --- a/apisix/cli/snippet.lua +++ b/apisix/cli/snippet.lua @@ -15,6 +15,7 @@ -- limitations under the License. -- local template = require("resty.template") +local pl_path = require("pl.path") local ipairs = ipairs @@ -77,6 +78,10 @@ function _M.generate_conf_server(env, conf) proxy_ssl_name $upstream_host; {% end %} proxy_ssl_protocols TLSv1.2 TLSv1.3; + {% if client_cert then %} + proxy_ssl_certificate {* client_cert *}; + proxy_ssl_certificate_key {* client_cert_key *}; + {% end %} {% else %} proxy_pass http://apisix_conf_backend; {% end %} @@ -92,10 +97,21 @@ function _M.generate_conf_server(env, conf) } } ]]) + + local tls = etcd.tls + local client_cert + local client_cert_key + if tls and tls.cert then + client_cert = pl_path.abspath(tls.cert) + client_cert_key = pl_path.abspath(tls.key) + end + return conf_render({ sni = etcd.tls and etcd.tls.sni, enable_https = enable_https, home = env.apisix_home or ".", + client_cert = client_cert, + client_cert_key = client_cert_key, }) end diff --git a/t/cli/test_deployment_traditional.sh b/t/cli/test_deployment_traditional.sh index 89567511848e..6a89ca0a65f4 100755 --- a/t/cli/test_deployment_traditional.sh +++ b/t/cli/test_deployment_traditional.sh @@ -104,6 +104,7 @@ deployment: make run sleep 1 +make stop if grep '\[error\]' logs/error.log; then echo "failed: could not connect to etcd with stream enabled" @@ -131,3 +132,55 @@ if ! echo "$out" | grep 'all nodes in the etcd cluster should enable/disable TLS fi echo "passed: validate etcd host" + +# The 'admin.apisix.dev' is injected by ci/common.sh@set_coredns + +# etcd mTLS verify +echo ' +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + host: + - "https://admin.apisix.dev:22379" + prefix: "/apisix" + tls: + cert: t/certs/mtls_client.crt + key: t/certs/mtls_client.key + verify: false + ' > conf/config.yaml + +make run +sleep 1 + +code=$(curl -o /dev/null -s -w %{http_code} http://127.0.0.1:9080/apisix/admin/routes -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1') +make stop + +if [ ! $code -eq 200 ]; then + echo "failed: could not work when mTLS is enabled" + exit 1 +fi + +echo "passed: etcd enables mTLS successfully" + +echo ' +deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + host: + - "https://admin.apisix.dev:22379" + prefix: "/apisix" + tls: + verify: false + ' > conf/config.yaml + +out=$(make init 2>&1 || echo "ouch") +if ! echo "$out" | grep "bad certificate"; then + echo "failed: apisix should echo \"bad certificate\"" + exit 1 +fi + +echo "passed: certificate verify fail expectedly" From cd69885596fb4901c8f0a1afbb3c02e04e32e4ff Mon Sep 17 00:00:00 2001 From: feihan <97138894+hf400159@users.noreply.github.com> Date: Thu, 30 Jun 2022 13:44:35 +0800 Subject: [PATCH 61/63] docs: fix error link (#7356) --- conf/config-default.yaml | 2 +- docs/en/latest/admin-api.md | 2 +- docs/en/latest/mtls.md | 2 +- docs/en/latest/plugins/gzip.md | 2 +- docs/en/latest/plugins/real-ip.md | 2 +- docs/en/latest/wasm.md | 2 +- docs/en/latest/xrpc/redis.md | 2 +- docs/zh/latest/admin-api.md | 2 +- docs/zh/latest/mtls.md | 2 +- docs/zh/latest/plugins/client-control.md | 2 +- docs/zh/latest/plugins/gzip.md | 2 +- docs/zh/latest/plugins/proxy-control.md | 2 +- docs/zh/latest/plugins/real-ip.md | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/conf/config-default.yaml b/conf/config-default.yaml index f35ec65b03c7..f03d31baca3a 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -289,7 +289,7 @@ etcd: #password: 5tHkHhYkjr6cQY # root password for etcd tls: # To enable etcd client certificate you need to build APISIX-Base, see - # https://apisix.apache.org/docs/apisix/FAQ#how-do-i-build-the-apisix-base-environment? + # https://apisix.apache.org/docs/apisix/FAQ#how-do-i-build-the-apisix-base-environment #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/admin-api.md b/docs/en/latest/admin-api.md index 0dc4ee318b5f..deaf1c4c7ce1 100644 --- a/docs/en/latest/admin-api.md +++ b/docs/en/latest/admin-api.md @@ -565,7 +565,7 @@ The following should be considered when setting the `hash_on` value: - When set to `vars_combinations`, the `key` is required. The value of the key can be a combination of any of the [Nginx variables](http://nginx.org/en/docs/varindex.html) like `$request_uri$remote_addr`. - When no value is set for either `hash_on` or `key`, the key defaults to `remote_addr`. -The features described below requires APISIX to be run on [APISIX-Base](./FAQ.md#how-do-i-build-the-apisix-base-environment?): +The features described below requires APISIX to be run on [APISIX-Base](./FAQ.md#how-do-i-build-the-apisix-base-environment): You can set the `scheme` to `tls`, which means "TLS over TCP". diff --git a/docs/en/latest/mtls.md b/docs/en/latest/mtls.md index b46e7d7b81e2..294d4b162fbf 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-Base](./FAQ.md#how-do-i-build-the-apisix-base-environment?) and configure `etcd.tls` section if you want APISIX to work on an etcd cluster with mTLS enabled. +You need to build [APISIX-Base](./FAQ.md#how-do-i-build-the-apisix-base-environment) 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/plugins/gzip.md b/docs/en/latest/plugins/gzip.md index 3096083c2b1b..69b7df762e3a 100644 --- a/docs/en/latest/plugins/gzip.md +++ b/docs/en/latest/plugins/gzip.md @@ -32,7 +32,7 @@ The `gzip` Plugin dynamically sets the behavior of [gzip in Nginx](https://docs. :::info IMPORTANT -This Plugin requires APISIX to run on [APISIX-Base](../FAQ.md#how-do-i-build-the-apisix-base-environment?). +This Plugin requires APISIX to run on [APISIX-Base](../FAQ.md#how-do-i-build-the-apisix-base-environment). ::: diff --git a/docs/en/latest/plugins/real-ip.md b/docs/en/latest/plugins/real-ip.md index f1f59559c8e5..88d7783f9423 100644 --- a/docs/en/latest/plugins/real-ip.md +++ b/docs/en/latest/plugins/real-ip.md @@ -35,7 +35,7 @@ This is more flexible but functions similarly to Nginx's [ngx_http_realip_module :::info IMPORTANT -This Plugin requires APISIX to run on [APISIX-Base](../FAQ.md#how-do-i-build-the-apisix-base-environment?). +This Plugin requires APISIX to run on [APISIX-Base](../FAQ.md#how-do-i-build-the-apisix-base-environment). ::: diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md index c8881df84547..506207303a8f 100644 --- a/docs/en/latest/wasm.md +++ b/docs/en/latest/wasm.md @@ -23,7 +23,7 @@ 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-Base](./FAQ.md#how-do-i-build-the-apisix-base-environment?), and is under construction. +This plugin requires APISIX to run on [APISIX-Base](./FAQ.md#how-do-i-build-the-apisix-base-environment), 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 diff --git a/docs/en/latest/xrpc/redis.md b/docs/en/latest/xrpc/redis.md index 63f79172462f..26c4added22b 100644 --- a/docs/en/latest/xrpc/redis.md +++ b/docs/en/latest/xrpc/redis.md @@ -35,7 +35,7 @@ The Redis protocol support allows APISIX to proxy Redis commands, and provide va :::note -This feature requires APISIX to be run on [APISIX-Base](../FAQ.md#how-do-i-build-the-apisix-base-environment?). +This feature requires APISIX to be run on [APISIX-Base](../FAQ.md#how-do-i-build-the-apisix-base-environment). It also requires the data sent from clients are well-formed and sane. Therefore, it should only be used in deployments where both the downstream and upstream are trusted. diff --git a/docs/zh/latest/admin-api.md b/docs/zh/latest/admin-api.md index 2ae3290de5d3..e5dc14202727 100644 --- a/docs/zh/latest/admin-api.md +++ b/docs/zh/latest/admin-api.md @@ -583,7 +583,7 @@ APISIX 的 Upstream 除了基本的负载均衡算法选择外,还支持对上 `keepalive_pool` 允许 upstream 对象有自己单独的连接池。 它下属的字段,比如 `requests`,可以用了配置上游连接保持的参数。 -这个特性需要 APISIX 运行于 [APISIX-Base](./FAQ.md#如何构建-APISIX-Base-环境?)。 +这个特性需要 APISIX 运行于 [APISIX-Base](./FAQ.md#如何构建-apisix-base-环境)。 **upstream 对象 json 配置内容:** diff --git a/docs/zh/latest/mtls.md b/docs/zh/latest/mtls.md index 07ab50e3183f..8996f2b5fef4 100644 --- a/docs/zh/latest/mtls.md +++ b/docs/zh/latest/mtls.md @@ -154,7 +154,7 @@ curl --resolve 'mtls.test.com::' "https:// Date: Thu, 30 Jun 2022 13:48:03 +0800 Subject: [PATCH 62/63] docs: add source build apisix link. (#7357) --- docs/en/latest/installation-guide.md | 7 +++++++ docs/zh/latest/installation-guide.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/docs/en/latest/installation-guide.md b/docs/en/latest/installation-guide.md index 9f3bda5a3282..40d9e44e472d 100644 --- a/docs/en/latest/installation-guide.md +++ b/docs/en/latest/installation-guide.md @@ -43,6 +43,7 @@ APISIX can be installed by the different methods listed below: {label: 'Docker', value: 'docker'}, {label: 'Helm', value: 'helm'}, {label: 'RPM', value: 'rpm'}, + {label: 'Source Code', value: 'source code'}, ]}> @@ -166,6 +167,12 @@ Run `apisix help` to get a list of all available operations. ::: + + + + +If you want to build APISIX from source, please refer to [Building APISIX from source](./building-apisix.md). + diff --git a/docs/zh/latest/installation-guide.md b/docs/zh/latest/installation-guide.md index 14b4e5f1ffd9..f563179cb22c 100644 --- a/docs/zh/latest/installation-guide.md +++ b/docs/zh/latest/installation-guide.md @@ -44,6 +44,7 @@ import TabItem from '@theme/TabItem'; {label: 'Docker', value: 'docker'}, {label: 'Helm', value: 'helm'}, {label: 'RPM', value: 'rpm'}, + {label: 'Source Code', value: 'source code'}, ]}> @@ -169,6 +170,12 @@ apisix start ::: + + + + +如果你想要使用源码构建 APISIX,请参考[源码安装 APISIX](./building-apisix.md)。 + From 7569eb99326f72f69a6e275126decce2b7e486d1 Mon Sep 17 00:00:00 2001 From: Navendu Pottekkat Date: Thu, 30 Jun 2022 12:00:46 +0530 Subject: [PATCH 63/63] docs: update "Other protocols" Plugins (#7312) --- docs/en/latest/config.json | 2 +- docs/en/latest/plugins/dubbo-proxy.md | 85 +++++++++++++-------------- docs/en/latest/plugins/mqtt-proxy.md | 56 ++++++++++-------- 3 files changed, 74 insertions(+), 69 deletions(-) diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 7fe61986b283..46c6ab4e9ce6 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -171,7 +171,7 @@ }, { "type": "category", - "label": "Other Protocols", + "label": "Other protocols", "items": [ "plugins/dubbo-proxy", "plugins/mqtt-proxy", diff --git a/docs/en/latest/plugins/dubbo-proxy.md b/docs/en/latest/plugins/dubbo-proxy.md index fb8873595039..c69dd6117776 100644 --- a/docs/en/latest/plugins/dubbo-proxy.md +++ b/docs/en/latest/plugins/dubbo-proxy.md @@ -1,5 +1,12 @@ --- title: dubbo-proxy +keywords: + - APISIX + - API Gateway + - Plugin + - Apache Dubbo + - dubbo-proxy +description: This document contains information about the Apache APISIX dubbo-proxy Plugin. ---