Skip to content

Commit

Permalink
doc: add doc for how to write plugin.(#909)
Browse files Browse the repository at this point in the history
  • Loading branch information
soulsoul authored and moonming committed Nov 27, 2019
1 parent a47353d commit f76fcec
Show file tree
Hide file tree
Showing 4 changed files with 378 additions and 0 deletions.
2 changes: 2 additions & 0 deletions FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Yes, in version 0.6 we have dashboard built in, you can operate APISIX through t

Of course, APISIX provides flexible custom plugins for developers and businesses to write their own logic.

[How to write plugin](doc/plugins/plugin-develop.md)

## Why we choose etcd as the configuration center?

For the configuration center, configuration storage is only the most basic function, and APISIX also needs the following features:
Expand Down
2 changes: 2 additions & 0 deletions FAQ_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ APISIX 是当前性能最好的 API 网关,单核 QPS 达到 2.3 万,平均

当然可以,APISIX 提供了灵活的自定义插件,方便开发者和企业编写自己的逻辑。

[如何开发插件](doc/plugins/plugin-develop-cn.md)

## 我们为什么选择 etcd 作为配置中心?

对于配置中心,配置存储只是最基本功能,APISIX 还需要下面几个特性:
Expand Down
183 changes: 183 additions & 0 deletions doc/plugins/plugin-develop-cn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
-->
[English](plugin-develop.md)

# 目录
- [**检查外部依赖**](#检查外部依赖)
- [**插件命名与配置**](#插件命名与配置)
- [**配置描述与校验**](#配置描述与校验)
- [**确定执行阶段**](#确定执行阶段)
- [**编写执行逻辑**](#编写执行逻辑)
- [**编写测试用例**](#编写测试用例)


## 检查外部依赖

如果你的插件,涉及到一些外部的依赖和三方库,请首先检查一下依赖项的内容。 如果插件需要用到共享内存,需要在 __bin/apisix__
件里面进行申明,例如:

```nginx
lua_shared_dict plugin-limit-req 10m;
lua_shared_dict plugin-limit-count 10m;
lua_shared_dict prometheus-metrics 10m;
lua_shared_dict plugin-limit-conn 10m;
lua_shared_dict upstream-healthcheck 10m;
lua_shared_dict worker-events 10m;
# for openid-connect plugin
lua_shared_dict discovery 1m; # cache for discovery metadata documents
lua_shared_dict jwks 1m; # cache for JWKs
lua_shared_dict introspection 10m; # cache for JWT verification results
```

插件本身提供了 init 方法。方便插件加载后做初始化动作。

注:如果部分插件的功能实现,需要在 Nginx 初始化启动,则可能需要在 __lua/apisix.lua__ 文件的初始化方法 http_init 中添加逻辑,并且
可能需要在 __bin/apisix__ 文件中,对 Nginx 配置文件生成的部分,添加一些你需要的处理。但是这样容易对全局产生影响,根据现有的
插件机制,我们不建议这样做,除非你已经对代码完全掌握。

## 插件命名与配置

给插件取一个很棒的名字,确定插件的加载优先级,然后在 __conf/config.yaml__ 文件中添加上你的插件名。例如 key-auth 这个插件,
需要在代码里指定插件名称(名称是插件的唯一标识,不可重名),在 __lua/apisix/plugins/key-auth.lua__ 文件中可以看到:

```lua
local plugin_name = "key-auth"

local _M = {
version = 0.1,
priority = 2500,
type = 'auth',
name = plugin_name,
schema = schema,
}
```

注:新插件的优先级( priority 属性 )不能与现有插件的优先级相同。

__conf/config.yaml__ 配置文件中,列出了启用的插件(都是以插件名指定的):

```yaml
plugins: # plugin list
- example-plugin
- limit-req
- limit-count
- limit-conn
- key-auth
- prometheus
- node-status
- jwt-auth
- zipkin
- ip-restriction
- grpc-transcode
- serverless-pre-function
- serverless-post-function
- openid-connect
- proxy-rewrite
- redirect
```
注:先后顺序与执行顺序无关。
## 配置描述与校验
定义插件的配置项,以及对应的 [Json Schema](https://json-schema.org) 描述,并完成对 json 的校验,这样方便对配置的数据规
格进行验证,以确保数据的完整性以及程序的健壮性。同样,我们以 key-auth 插件为例,看看他的配置数据:
```json
"key-auth" : {
"key" : "auth-one"
}
```

插件的配置数据比较简单,只支持一个命名为 key 的属性,那么我们看下他的 Schema 描述:

```lua
local schema = {
type = "object",
properties = {
key = {type = "string"},
}
}
```

同时,需要实现 __check_schema(conf)__ 方法,完成配置参数的合法性校验。

```lua
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
```

注:项目已经提供了 __core.schema.check__ 公共方法,直接使用即可完成配置参数校验。

## 确定执行阶段

根据业务功能,确定你的插件需要在哪个阶段执行。 key-auth 是一个认证插件,只要在请求进来之后业务响应之前完成认证即可。
该插件在 rewrite 、access 阶段执行都可以,项目中是用 rewrite 阶段执行认证逻辑,一般 IP 准入、接口权限是在 access 阶段
完成的。

## 编写执行逻辑

在对应的阶段方法里编写功能的逻辑代码。

## 编写测试用例

针对功能,完善各种维度的测试用例,对插件做个全方位的测试吧!插件的测试用例,都在 __t/plugin__ 目录下,可以前去了解。
项目测试框架采用的 [****test-nginx****](https://github.com/openresty/test-nginx)
一个测试用例 __.t__ 文件,通常用 \__DATA\__ 分割成 序言部分 和 数据部分。这里我们简单介绍下数据部分,
也就是真正测试用例的部分,仍然以 key-auth 插件为例:

```perl
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.key-auth")
local ok, err = plugin.check_schema({key = 'test-key'})
if not ok then
ngx.say(err)
end

ngx.say("done")
}
}
--- request
GET /t
--- response_body
done
--- no_error_log
[error]
```

一个测试用例主要有三部分内容:
- 程序代码: Nginx location 的配置内容
- 输入: http 的 request 信息
- 输出检查: status ,header ,body ,error_log 检查

这里请求 __/t__ ,经过配置文件 __location__ ,调用 __content_by_lua_block__ 指令完成 lua 的脚本,最终返回。
用例的断言是 response_body 返回 "done",__no_error_log__ 表示会对 Nginx 的 error.log 检查,
必须没有 ERROR 级别的记录。

### 附上test-nginx 执行流程:

根据我们在 Makefile 里配置的 PATH,和每一个 __.t__ 文件最前面的一些配置项,框架会组装成一个完整的 nginx.conf 文件,
__t/servroot__ 会被当成 Nginx 的工作目录,启动 Nginx 实例。根据测试用例提供的信息,发起 http 请求并检查 http 的返回项,
包括 http status,http response header, http response body 等。

191 changes: 191 additions & 0 deletions doc/plugins/plugin-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<!--
#
# 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.
#
-->
[中文](plugin-develop-cn.md)

# table of contents
- [**check dependencies**](#check-dependencies)
- [**name and config**](#name-and-config)
- [**schema and check**](#schema-and-check)
- [**choose phase to run**](#choose-phase-to-run)
- [**implement the logic**](#implement-the-logic)
- [**write test case**](#write-test-case)


## check dependencies

if you have dependencies on external libraries , check the dependent items . if your plugin needs to use shared memory , it
needs to declare in __bin/apisix__ , for example :

```nginx
lua_shared_dict plugin-limit-req 10m;
lua_shared_dict plugin-limit-count 10m;
lua_shared_dict prometheus-metrics 10m;
lua_shared_dict plugin-limit-conn 10m;
lua_shared_dict upstream-healthcheck 10m;
lua_shared_dict worker-events 10m;
# for openid-connect plugin
lua_shared_dict discovery 1m; # cache for discovery metadata documents
lua_shared_dict jwks 1m; # cache for JWKs
lua_shared_dict introspection 10m; # cache for JWT verification results
```

The plugin itself provides the init method . It is convenient for plugins to perform some initialization after
the plugin is loaded .

Note : if the dependency of some plugin needs to be initialized when Nginx start , you may need to add logic to the initialization
method "http_init" in the file __Lua/apifix.lua__ , And you may need to add some processing on generated part of Nginx
configuration file in __bin/apisix__ file . but it is easy to have an impact on the overall situation according to the
existing plugin mechanism, we do not recommend this unless you have a complete grasp of the code .

## name and config

determine the name and priority of the plugin , and add to conf/config.yaml . For example , for the key-auth plugin ,
you need to specify the plugin name in the code (the name is the unique identifier of the plugin and cannot be
duplicate) , you can see the code in file "__lua/apisix/plugins/key-auth.lua__" :

```lua
local plugin_name = "key-auth"

local _M = {
version = 0.1,
priority = 2500,
type = 'auth',
name = plugin_name,
schema = schema,
}
```

Note : The priority of the new plugin cannot be the same as the priority of any existing plugin.

in the "__conf/config.yaml__" configuration file , the enabled plugins (all specified by plugin name) are listed .

```yaml
plugins: # plugin list
- example-plugin
- limit-req
- limit-count
- limit-conn
- key-auth
- prometheus
- node-status
- jwt-auth
- zipkin
- ip-restriction
- grpc-transcode
- serverless-pre-function
- serverless-post-function
- openid-connect
- proxy-rewrite
- redirect
```
Note : the order of the plugins is not related to the order of execution .
## schema and check
Write [Json Schema](https://json-schema.org) descriptions and check functions. similarly , take the key-auth plugin as an example to see its
configuration data :
```json
"key-auth" : {
"key" : "auth-one"
}
```

The configuration data of the plugin is relatively simple . Only one attribute named key is supported . Let's look
at its schema description :

```lua
local schema = {
type = "object",
properties = {
key = {type = "string"},
}
}
```

at the same time, we need to implement the __check_schema(conf)__ method to complete the specification verification .

```lua
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
```

Note: the project has provided the public method "__core.schema.check__" , which can be used directly to complete JSON
verification .

## choose phase to run

determine which phase to run , generally access or rewrite . if you don't know the Openresty life cycle , it's
recommended to know it in advance . key-auth is an authentication plugin , as long as the authentication is completed
before the business response after the request comes in . The plugin can be executed in the rewrite and access phases ,
in the project, the authentication logic is implemented in the rewrite phase . Generally, IP access and interface
permission are completed in the access phase .

## implement the logic

Write the logic of the plugin in the corresponding phase .

## write test case

for functions , write and improve the test cases of various dimensions , do a comprehensive test for your plugin ! The
test cases of plugins are all in the "__t/plugin__" directory. You can go ahead to find out . the test framework
[****test-nginx****](https://github.com/openresty/test-nginx) adopted by the project. a test case, .t file is usually
divided into prologue and data parts by \__data\__ . Here we will briefly introduce the data part, that is, the part
of the real test case . For example, the key-auth plugin :

```perl
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.key-auth")
local ok, err = plugin.check_schema({key = 'test-key'})
if not ok then
ngx.say(err)
end

ngx.say("done")
}
}
--- request
GET /t
--- response_body
done
--- no_error_log
[error]
```

a test case consists of three parts :
- __Program code__ : configuration content of Nginx location
- __Input__ : http request information
- __Output check__ : status, header, body, error log check

when we request __/t__ , which config in the configuration file , the Nginx will call "__content_by_lua_block__" instruction to
complete the Lua script, and finally return. The assertion of the use case is response_body return "done",
"__no_error_log__" means to check the "__error.log__" of Nginx. There must be no ERROR level record .

### Attach the test-nginx execution process:

According to the path we configured in the makefile and some configuration items at the front of each __.t__ file, the
framework will assemble into a complete nginx.conf file. "__t/servroot__" is the working directory of Nginx and start the
Nginx instance. according to the information provided by the test case, initiate the http request and check that the
return items of HTTP include HTTP status, HTTP response header, HTTP response body and so on .

0 comments on commit f76fcec

Please sign in to comment.