| --- |
| title: Plugin Develop |
| --- |
| |
| <!-- |
| # |
| # 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 documentation is about developing plugin in Lua. For other languages, |
| see [external plugin](./external-plugin.md). |
| |
| ## Where to put your plugins |
| |
| Use the `extra_lua_path` parameter in `conf/config.yaml` file to load your custom plugin code (or use `extra_lua_cpath` for compiled `.so` or `.dll` file). |
| |
| For example, you can create a directory `/path/to/example`: |
| |
| ```yaml |
| apisix: |
| ... |
| extra_lua_path: "/path/to/example/?.lua" |
| ``` |
| |
| The structure of the `example` directory should look like this: |
| |
| ``` |
| ├── example |
| │ └── apisix |
| │ ├── plugins |
| │ │ └── 3rd-party.lua |
| │ └── stream |
| │ └── plugins |
| │ └── 3rd-party.lua |
| ``` |
| |
| :::note |
| |
| The directory (`/path/to/example`) must contain the `/apisix/plugins` subdirectory. |
| |
| ::: |
| |
| ## Enable the plugin |
| |
| To enable your custom plugin, add the plugin list to `conf/config.yaml` and append your plugin name. For instance: |
| |
| ```yaml |
| plugins: # See `conf/config.yaml.example` for an example |
| - ... # Add existing plugins |
| - your-plugin # Add your custom plugin name (name is the plugin name defined in the code) |
| ``` |
| |
| :::warning |
| |
| In particular, most APISIX plugins are enabled by default when the plugins field configuration is not defined (The default enabled plugins can be found in [apisix/cli/config.lua](https://github.com/apache/apisix/blob/master/apisix/cli/config.lua)). |
| |
| Once the plugins configuration is defined in `conf/config.yaml`, the new plugins list will replace the default configuration instead of merging. Therefore, when defining the `plugins` field, make sure to include the built-in plugins that are being used. To maintain consistency with the default behavior, you can include all the default enabled plugins defined in `apisix/cli/config.lua`. |
| |
| ::: |
| |
| ## Writing plugins |
| |
| The [`example-plugin`](https://github.com/apache/apisix/blob/master/apisix/plugins/example-plugin.lua) plugin in this repo provides an example. |
| |
| ### Naming and priority |
| |
| Specify the plugin name (the name is the unique identifier of the plugin and cannot be duplicate) and priority in the code. |
| |
| ```lua |
| local plugin_name = "example-plugin" |
| |
| local _M = { |
| version = 0.1, |
| priority = 0, |
| name = plugin_name, |
| schema = schema, |
| metadata_schema = metadata_schema, |
| } |
| ``` |
| |
| Note: The priority of the new plugin cannot be same to any existing ones, you can use the `/v1/schema` method of [control API](./control-api.md#get-v1schema) to view the priority of all plugins. In addition, plugins with higher priority value will be executed first in a given phase (see the definition of `phase` in [choose-phase-to-run](#choose-phase-to-run)). For example, the priority of example-plugin is 0 and the priority of ip-restriction is 3000. Therefore, the ip-restriction plugin will be executed first, then the example-plugin plugin. It's recommended to use priority 1 ~ 99 for your plugin unless you want it to run before some builtin plugins. |
| |
| 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 example-plugin plugin as an example to see its |
| configuration data: |
| |
| ```json |
| { |
| "example-plugin": { |
| "i": 1, |
| "s": "s", |
| "t": [1] |
| } |
| } |
| ``` |
| |
| Let's look at its schema description : |
| |
| ```lua |
| local schema = { |
| type = "object", |
| properties = { |
| i = {type = "number", minimum = 0}, |
| s = {type = "string"}, |
| t = {type = "array", minItems = 1}, |
| ip = {type = "string"}, |
| port = {type = "integer"}, |
| }, |
| required = {"i"}, |
| } |
| ``` |
| |
| The schema defines a non-negative number `i`, a string `s`, a non-empty array of `t`, and `ip` / `port`. Only `i` is required. |
| |
| At the same time, we need to implement the __check_schema(conf, schema_type)__ method to complete the specification verification. |
| |
| ```lua |
| function _M.check_schema(conf) |
| return core.schema.check(schema, conf) |
| end |
| ``` |
| |
| :::note |
| |
| Note: the project has provided the public method "__core.schema.check__", which can be used directly to complete JSON |
| verification. |
| |
| ::: |
| |
| The input parameter **schema_type** is used to distinguish between different schemas types. For example, many plugins need to use some [metadata](./terminology/plugin-metadata.md), so they define the plugin's `metadata_schema`. |
| |
| ```lua title="example-plugin.lua" |
| -- schema definition for metadata |
| local metadata_schema = { |
| type = "object", |
| properties = { |
| ikey = {type = "number", minimum = 0}, |
| skey = {type = "string"}, |
| }, |
| required = {"ikey", "skey"}, |
| } |
| |
| function _M.check_schema(conf, schema_type) |
| --- check schema for metadata |
| if schema_type == core.schema.TYPE_METADATA then |
| return core.schema.check(metadata_schema, conf) |
| end |
| return core.schema.check(schema, conf) |
| end |
| ``` |
| |
| Another example, the [key-auth](https://github.com/apache/apisix/blob/master/apisix/plugins/key-auth.lua) plugin needs to provide a `consumer_schema` to check the configuration of the `plugins` attribute of the `consumer` resource in order to be used with the [Consumer](./admin-api.md#consumer) resource. |
| |
| ```lua title="key-auth.lua" |
| |
| local consumer_schema = { |
| type = "object", |
| properties = { |
| key = {type = "string"}, |
| }, |
| required = {"key"}, |
| } |
| |
| function _M.check_schema(conf, schema_type) |
| if schema_type == core.schema.TYPE_CONSUMER then |
| return core.schema.check(consumer_schema, conf) |
| else |
| return core.schema.check(schema, conf) |
| end |
| end |
| ``` |
| |
| ### Choose phase to run |
| |
| Determine which [phase](./terminology/plugin.md#plugins-execution-lifecycle) to run, generally access or rewrite. If you don't know the [OpenResty lifecycle](https://github.com/openresty/lua-nginx-module/blob/master/README.markdown#directives), it's |
| recommended to learn about it in advance. For example `key-auth` is an authentication plugin, thus the authentication should be completed |
| before forwarding the request to any upstream service. Therefore, the plugin must be executed in the rewrite phases. |
| Similarly, if you want to modify or process the response body or headers you can do that in the `body_filter` or in the `header_filter` phases respectively. |
| |
| The following code snippet shows how to implement any logic relevant to the plugin in the OpenResty log phase. |
| |
| ```lua |
| function _M.log(conf, ctx) |
| -- Implement logic here |
| end |
| ``` |
| |
| **Note : we can't invoke `ngx.exit`, `ngx.redirect` or `core.respond.exit` in rewrite phase and access phase. if need to exit, just return the status and body, the plugin engine will make the exit happen with the returned status and body. [example](https://github.com/apache/apisix/blob/35269581e21473e1a27b11cceca6f773cad0192a/apisix/plugins/limit-count.lua#L177)** |
| |
| ### extra phase |
| |
| Besides OpenResty's phases, we also provide extra phases to satisfy specific purpose: |
| |
| * `delayed_body_filter` |
| |
| ```lua |
| function _M.delayed_body_filter(conf, ctx) |
| -- delayed_body_filter is called after body_filter |
| -- it is used by the tracing plugins to end the span right after body_filter |
| end |
| ``` |
| |
| ### Implement the logic |
| |
| Write the logic of the plugin in the corresponding phase. There are two parameters `conf` and `ctx` in the phase method, take the `limit-conn` plugin configuration as an example. |
| |
| #### conf parameter |
| |
| The `conf` parameter is the relevant configuration information of the plugin, you can use `core.log.warn(core.json.encode(conf))` to output it to `error.log` for viewing, as shown below: |
| |
| ```lua |
| function _M.access(conf, ctx) |
| core.log.warn(core.json.encode(conf)) |
| ...... |
| end |
| ``` |
| |
| conf: |
| |
| ```json |
| { |
| "rejected_code": 503, |
| "burst": 0, |
| "default_conn_delay": 0.1, |
| "conn": 1, |
| "key": "remote_addr" |
| } |
| ``` |
| |
| #### ctx parameter |
| |
| The `ctx` parameter caches data information related to the request. You can use `core.log.warn(core.json.encode(ctx, true))` to output it to `error.log` for viewing, as shown below : |
| |
| ```lua |
| function _M.access(conf, ctx) |
| core.log.warn(core.json.encode(ctx, true)) |
| ...... |
| end |
| ``` |
| |
| ### Others |
| |
| If your plugin has a new code directory of its own, and you need to redistribute it with the APISIX source code, you will need to modify the `Makefile` to create directory, such as: |
| |
| ``` |
| $(INSTALL) -d $(INST_LUADIR)/apisix/plugins/skywalking |
| $(INSTALL) apisix/plugins/skywalking/*.lua $(INST_LUADIR)/apisix/plugins/skywalking/ |
| ``` |
| |
| There are other fields in the `_M` which affect the plugin's behavior. |
| |
| ```lua |
| local _M = { |
| ... |
| type = 'auth', |
| run_policy = 'prefer_route', |
| } |
| ``` |
| |
| `run_policy` field can be used to control the behavior of the plugin execution. |
| When this field set to `prefer_route`, and the plugin has been configured both |
| in the global and at the route level, only the route level one will take effect. |
| |
| `type` field is required to be set to `auth` if your plugin needs to work with consumer. |
| |
| ## Load plugin and replace plugin |
| |
| Using `require "apisix.plugins.3rd-party"` will load your plugin, just like `require "apisix.plugins.jwt-auth"` will load the `jwt-auth` plugin. |
| |
| Sometimes you may want to override a method instead of a whole file. In this case, you can configure `lua_module_hook` in `conf/config.yaml` |
| to introduce your hook. |
| |
| Assume that your configuration is as follows: |
| |
| ```yaml |
| apisix: |
| ... |
| extra_lua_path: "/path/to/example/?.lua" |
| lua_module_hook: "my_hook" |
| ``` |
| |
| The `example/my_hook.lua` will be loaded when APISIX starts, and you can use this hook to replace a method in APISIX. |
| The example of [my_hook.lua](https://github.com/apache/apisix/blob/master/example/my_hook.lua) can be found under the `example` directory of this project. |
| |
| ## Check external dependencies |
| |
| If you have dependencies on external libraries, check the dependent items. If your plugin needs to use shared memory, it |
| needs to declare via [customizing Nginx configuration](./customize-nginx-configuration.md), for example : |
| |
| ```yaml |
| # put this in config.yaml: |
| nginx_config: |
| http_configuration_snippet: | |
| # 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. If you need to clean up the initialization, you can put it in the corresponding destroy method. |
| |
| 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 `apisix/init.lua`, and you may need to add some processing on generated part of Nginx |
| configuration file in `apisix/cli/ngx_tpl.lua` 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**. |
| |
| ## Encrypted storage fields |
| |
| Some plugins require parameters to be stored encrypted, such as the `password` parameter of the `basic-auth` plugin. This plugin needs to specify in the `schema` which parameters need to be stored encrypted. |
| |
| ```lua |
| encrypt_fields = {"password"} |
| ``` |
| |
| If it is a nested parameter, such as the `clickhouse.password` parameter of the `error-log-logger` plugin, it needs to be separated by `.`: |
| |
| ```lua |
| encrypt_fields = {"clickhouse.password"} |
| ``` |
| |
| Currently not supported yet: |
| |
| 1. more than two levels of nesting |
| 2. fields in arrays |
| |
| Parameters can be stored encrypted by specifying `encrypt_fields = {"password"}` in the `schema`. APISIX will provide the following functionality. |
| |
| - When adding and updating resources, APISIX automatically encrypts the parameters declared in `encrypt_fields` and stores them in etcd |
| - When fetching resources and when running the plugin, APISIX automatically decrypts the parameters declared in `encrypt_fields` |
| |
| By default, APISIX has `data_encryption` enabled with [two default keys](https://github.com/apache/apisix/blob/85563f016c35834763376894e45908b2fb582d87/apisix/cli/config.lua#L75), you can modify them in `config.yaml`. |
| |
| ```yaml |
| apisix: |
| data_encryption: |
| enable: true |
| keyring: |
| - ... |
| ``` |
| |
| APISIX will try to decrypt the data with keys in the order of the keys in the keyring (only for parameters declared in `encrypt_fields`). If the decryption fails, the next key will be tried until the decryption succeeds. |
| |
| If none of the keys in `keyring` can decrypt the data, the original data is used. |
| |
| ## Register public API |
| |
| A plugin can register API which exposes to the public. Take batch-requests plugin as an example, this plugin registers `POST /apisix/batch-requests` to allow developers to group multiple API requests into a single HTTP request/response cycle: |
| |
| ```lua |
| function batch_requests() |
| -- ... |
| end |
| |
| function _M.api() |
| -- ... |
| return { |
| { |
| methods = {"POST"}, |
| uri = "/apisix/batch-requests", |
| handler = batch_requests, |
| } |
| } |
| end |
| ``` |
| |
| Note that the public API will not be exposed by default, you will need to use the [public-api plugin](plugins/public-api.md) to expose it. |
| |
| ## Register control API |
| |
| If you only want to expose the API to the localhost or intranet, you can expose it via [Control API](./control-api.md). |
| |
| Take a look at example-plugin plugin: |
| |
| ```lua |
| local function hello() |
| local args = ngx.req.get_uri_args() |
| if args["json"] then |
| return 200, {msg = "world"} |
| else |
| return 200, "world\n" |
| end |
| end |
| |
| |
| function _M.control_api() |
| return { |
| { |
| methods = {"GET"}, |
| uris = {"/v1/plugin/example-plugin/hello"}, |
| handler = hello, |
| } |
| } |
| end |
| ``` |
| |
| If you don't change the default control API configuration, the plugin will be expose `GET /v1/plugin/example-plugin/hello` which can only be accessed via `127.0.0.1`. Test with the following command: |
| |
| ```shell |
| curl -i -X GET "http://127.0.0.1:9090/v1/plugin/example-plugin/hello" |
| ``` |
| |
| [Read more about control API introduction](./control-api.md) |
| |
| ## Register custom variables |
| |
| We can use variables in many places of APISIX. For example, customizing log format in http-logger, using it as the key of `limit-*` plugins. In some situations, the builtin variables are not enough. Therefore, APISIX allows developers to register their variables globally, and use them as normal builtin variables. |
| |
| For instance, let's register a variable called `a6_labels_zone` to fetch the value of the `zone` label in a route: |
| |
| ``` |
| local core = require "apisix.core" |
| |
| core.ctx.register_var("a6_labels_zone", function(ctx) |
| local route = ctx.matched_route and ctx.matched_route.value |
| if route and route.labels then |
| return route.labels.zone |
| end |
| return nil |
| end) |
| ``` |
| |
| After that, any get operation to `$a6_labels_zone` will call the registered getter to fetch the value. |
| |
| Note that the custom variables can't be used in features that depend on the Nginx directive, like `access_log_format`. |
| |
| ## Write test cases |
| |
| 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. APISIX uses |
| [****test-nginx****](https://github.com/openresty/test-nginx) as the test framework. 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'}, core.schema.TYPE_CONSUMER) |
| if not ok then |
| ngx.say(err) |
| end |
| |
| ngx.say("done") |
| } |
| } |
| --- request |
| GET /t |
| --- response_body |
| done |
| --- no_error_log |
| [error] |
| ``` |
| |
| 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. The log files for the unit test |
| are located in the following folder: 't/servroot/logs'. |
| |
| The above test case represents a simple scenario. Most scenarios will require multiple steps to validate. To do this, create multiple tests `=== TEST 1`, `=== TEST 2`, and so on. These tests will be executed sequentially, allowing you to break down scenarios into a sequence of atomic steps. |
| |
| Additionally, there are some convenience testing endpoints which can be found [here](https://github.com/apache/apisix/blob/master/t/lib/server.lua#L36). For example, see [proxy-rewrite](https://github.com/apache/apisix/blob/master/t/plugin/proxy-rewrite.t). In test 42, the upstream `uri` is made to redirect `/test?new_uri=hello` to `/hello` (which always returns `hello world`). In test 43, the response body is confirmed to equal `hello world`, meaning the proxy-rewrite configuration added with test 42 worked correctly. |
| |
| Refer the following [document](building-apisix.md) to setup the testing framework. |
| |
| ### 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. |
| |
| ## Additional Resource(s) |
| |
| - Key Concepts - [Plugins](https://apisix.apache.org/docs/apisix/terminology/plugin/) |
| - [Apache APISIX Extensions Guide](https://apisix.apache.org/blog/2021/10/29/extension-guide/) |
| - [Create a Custom Plugin in Lua](https://docs.api7.ai/apisix/how-to-guide/custom-plugins/create-plugin-in-lua) |
| - [example-plugin code](https://github.com/apache/apisix/blob/master/apisix/plugins/example-plugin.lua) |