插件开发

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

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

    插件命名与配置

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

    1. local _M = {
    2. version = 0.1,
    3. priority = 0,
    4. name = plugin_name,
    5. schema = schema,
    6. metadata_schema = metadata_schema,
    7. }

    注:新插件的优先级( priority 属性 )不能与现有插件的优先级相同,您可以使用 的 /v1/schema 方法查看所有插件的优先级。另外,同一个阶段里面,优先级( priority )值大的插件,会优先执行,比如 example-plugin 的优先级是 0 ,ip-restriction 的优先级是 3000 ,所以在每个阶段,会先执行 ip-restriction 插件,再去执行 example-plugin 插件。这里的“阶段”的定义,参见后续的确定执行阶段这一节。对于你的插件,建议采用 1 到 99 之间的优先级。

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

    1. plugins: # plugin list
    2. - limit-req
    3. - limit-count
    4. - limit-conn
    5. - key-auth
    6. - prometheus
    7. - node-status
    8. - jwt-auth
    9. - zipkin
    10. - ip-restriction
    11. - grpc-transcode
    12. - serverless-pre-function
    13. - serverless-post-function
    14. - openid-connect
    15. - proxy-rewrite
    16. - redirect
    17. ...

    注:先后顺序与执行顺序无关。

    特别需要注意的是,如果你的插件有新建自己的代码目录,那么就需要修改 Makefile 文件,新增创建文件夹的操作,比如:

    1. $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/skywalking
    2. $(INSTALL) apisix/plugins/skywalking/*.lua $(INST_LUADIR)/apisix/plugins/skywalking/

    定义插件的配置项,以及对应的 描述,并完成对 json 的校验,这样方便对配置的数据规 格进行验证,以确保数据的完整性以及程序的健壮性。同样,我们以 example-plugin 插件为例,看看他的配置数据:

    1. {
    2. "example-plugin": {
    3. "i": 1,
    4. "s": "s",
    5. "t": [1]
    6. }
    7. }

    我们看下他的 Schema 描述:

    1. local schema = {
    2. type = "object",
    3. properties = {
    4. i = {type = "number", minimum = 0},
    5. t = {type = "array", minItems = 1},
    6. ip = {type = "string"},
    7. port = {type = "integer"},
    8. },
    9. required = {"i"},
    10. }

    这个 schema 定义了一个非负数 i,字符串 s,非空数组 t,和 ipport。只有 i 是必需的。

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

    另外,如果插件需要使用一些元数据,可以定义插件的 metadata_schema ,然后就可以通过 动态的管理这些元数据了。如:

    1. local metadata_schema = {
    2. type = "object",
    3. properties = {
    4. ikey = {type = "number", minimum = 0},
    5. skey = {type = "string"},
    6. },
    7. required = {"ikey", "skey"},
    8. additionalProperties = false,
    9. }
    10. local plugin_name = "example-plugin"
    11. local _M = {
    12. version = 0.1,
    13. priority = 0, -- TODO: add a type field, may be a good idea
    14. name = plugin_name,
    15. schema = schema,
    16. metadata_schema = metadata_schema,
    17. }

    你可能之前见过 key-auth 这个插件在它的模块定义时设置了 type = 'auth'。 当一个插件设置 type = 'auth',说明它是个认证插件。

    认证插件需要在执行后选择对应的 consumer。举个例子,在 key-auth 插件中,它通过 apikey 请求头获取对应的 consumer,然后通过 consumer.attach_consumer 设置它。

    为了跟 consumer 资源一起使用,认证插件需要提供一个 consumer_schema 来检验 consumer 资源的 plugins 属性里面的配置。

    下面是 key-auth 插件的 consumer 配置:

    1. {
    2. "username": "Joe",
    3. "plugins": {
    4. "key-auth": {
    5. "key": "Joe's key"
    6. }
    7. }
    8. }

    你在创建 Consumer 时会用到它。

    为了检验这个配置,这个插件使用了如下的 schema:

    1. local consumer_schema = {
    2. type = "object",
    3. additionalProperties = false,
    4. properties = {
    5. key = {type = "string"},
    6. },
    7. required = {"key"},
    8. }

    注意 key-auth 的 check_schema(conf) 方法和 example-plugin 的同名方法的区别:

    1. -- key-auth
    2. function _M.check_schema(conf, schema_type)
    3. if schema_type == core.schema.TYPE_CONSUMER then
    4. return core.schema.check(consumer_schema, conf)
    5. else
    6. return core.schema.check(schema, conf)
    7. end
    8. end
    1. -- example-plugin
    2. return core.schema.check(schema, conf)
    3. end

    确定执行阶段

    根据业务功能,确定你的插件需要在哪个阶段执行。 key-auth 是一个认证插件,所以需要在 rewrite 阶段执行。在 APISIX,只有认证逻辑可以在 rewrite 阶段里面完成,其他需要在代理到上游之前执行的逻辑都是在 access 阶段完成的。

    注意:我们不能在 rewrite 和 access 阶段调用 ngx.exit 或者 core.respond.exit。如果确实需要退出,只需要 return 状态码和正文,插件引擎将使用返回的状态码和正文进行退出。例子

    在对应的阶段方法里编写功能的逻辑代码,在阶段方法中具有 和 ctx 两个参数,以 limit-conn 插件配置为例。

    1. function _M.access(conf, ctx)
    2. core.log.warn(core.json.encode(conf))
    3. ......
    4. end

    conf:

    1. {
    2. "rejected_code": 503,
    3. "burst": 0,
    4. "default_conn_delay": 0.1,
    5. "conn": 1,
    6. "key": "remote_addr"
    7. }

    ctx 参数缓存了请求相关的数据信息,您可以通过 core.log.warn(core.json.encode(ctx, true)) 将其输出到 error.log 中进行查看,如下所示:

    1. function _M.access(conf, ctx)
    2. core.log.warn(core.json.encode(ctx, true))
    3. ......
    4. end

    编写测试用例

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

    1. === TEST 1: sanity
    2. --- config
    3. location /t {
    4. content_by_lua_block {
    5. local plugin = require("apisix.plugins.key-auth")
    6. local ok, err = plugin.check_schema({key = 'test-key'}, core.schema.TYPE_CONSUMER)
    7. if not ok then
    8. ngx.say(err)
    9. end
    10. ngx.say("done")
    11. }
    12. }
    13. --- request
    14. GET /t
    15. --- response_body
    16. done
    17. --- no_error_log
    18. [error]

    一个测试用例主要有三部分内容:

    • 程序代码: Nginx location 的配置内容
    • 输入: http 的 request 信息
    • 输出检查: status ,header ,body ,error_log 检查

    这里请求 /t ,经过配置文件 location ,调用 content_by_lua_block 指令完成 lua 的脚本,最终返回。 用例的断言是 responsebody 返回 “done”,_no_error_log 表示会对 Nginx 的 error.log 检查, 必须没有 ERROR 级别的记录。

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

    插件可以注册暴露给公网的接口。以 jwt-auth 插件为例,这个插件为了让客户端能够签名,注册了 GET /apisix/plugin/jwt/sign 这个接口:

    1. local function gen_token()
    2. -- ...
    3. end
    4. function _M.api()
    5. return {
    6. {
    7. methods = {"GET"},
    8. uri = "/apisix/plugin/jwt/sign",
    9. handler = gen_token,
    10. }
    11. end

    注意注册的接口会暴露到外网。 你可能需要使用 来保护它。

    如果你只想暴露 API 到 localhost 或内网,你可以通过 Control API 来暴露它。

    Take a look at example-plugin plugin:

    如果你没有改过默认的 control API 配置,这个插件暴露的 GET /v1/plugin/example-plugin/hello API 只有通过 才能访问它。