feat(apisix): add Cloudron package
- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: The Implementation of Plugin Runner
|
||||
---
|
||||
|
||||
<!--
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
-->
|
||||
|
||||
## Prerequirement
|
||||
|
||||
Each request which runs the extern plugin will trigger an RPC to Plugin Runner over a connection on Unix socket. The data of RPC are serialized with [Flatbuffers](https://github.com/google/flatbuffers).
|
||||
|
||||
Therefore, the Plugin Runner needs to:
|
||||
|
||||
1. handle a connection on Unix socket
|
||||
2. support Flatbuffers
|
||||
3. use the proto & generated code in https://github.com/api7/ext-plugin-proto/
|
||||
|
||||
## Listening to the Path
|
||||
|
||||
APISIX will pass the path of Unix socket as an environment variable `APISIX_LISTEN_ADDRESS` to the Plugin Runner. So the runner needs to read the value and listen to that address during starting.
|
||||
|
||||
## Register Plugins
|
||||
|
||||
The Plugin Runner should be able to load plugins written in the particular language.
|
||||
|
||||
## Handle RPC
|
||||
|
||||
There are two kinds of RPC: PrepareConf & HTTPReqCall
|
||||
|
||||
### Handle PrepareConf
|
||||
|
||||
As people can configure the extern plugin on the side of APISIX, we need a way to sync the plugin configuration to the Plugin Runner.
|
||||
|
||||
When there is a configuration that needs to sync to the Plugin Runner, we will send it via the PrepareConf RPC call. The Plugin Runner should be able to handle the call and store the configuration in a cache, then returns a unique conf token that represents the configuration.
|
||||
|
||||
In the previous design, an idempotent key is sent with the configuration. This field is deprecated and the Plugin Runner can safely ignore it.
|
||||
|
||||
Requests run plugins with particular configuration will bear a particular conf token in the RPC call, and the Plugin Runner is expected to look up actual configuration via the token.
|
||||
|
||||
When the configuration is modified, APISIX will send a new PrepareConf to the Plugin Runner. Currently, there is no way to notify the Plugin Runner that a configuration is removed. Therefore, we introduce another environment variable `APISIX_CONF_EXPIRE_TIME` as the conf cache expire time. The Plugin Runner should be able to cache the conf slightly longer than `APISIX_CONF_EXPIRE_TIME`, and APISIX will send another PrepareConf to refresh the cache if the configuration is still existing after `APISIX_CONF_EXPIRE_TIME` seconds.
|
||||
|
||||
### Handle HTTPReqCall
|
||||
|
||||
Each request which runs the extern plugin will trigger the HTTPReqCall. The HTTPReqCall is almost a serialized version of HTTP request, plus a conf token. The Plugin Runner is expected to tell APISIX what to update by the response of HTTPReqCall RPC call.
|
||||
|
||||
Sometimes the plugin in the Plugin Runner needs to know some information that is not part of the HTTPReqCall request, such as the request start time and the route ID in APISIX. Hence the Plugin Runner needs to reply to an `ExtraInfo` message as the response on the connection which sends the HTTPReqCall request. APISIX will read the `ExtraInfo` message and return the asked information.
|
||||
|
||||
Currently, the information below is passed by `ExtraInfo`:
|
||||
|
||||
* variable value
|
||||
* request body
|
||||
|
||||
The flow of HTTPReqCall procession is:
|
||||
|
||||
```
|
||||
APISIX sends HTTPReqCall
|
||||
Plugin Runner looks up the plugin configuration by the token in HTTPReqCall
|
||||
(optional) loop:
|
||||
Plugin Runner asks for ExtraInfo
|
||||
APISIX replies the ExtraInfo
|
||||
Plugin Runner replies HTTPReqCall
|
||||
```
|
@@ -0,0 +1,376 @@
|
||||
---
|
||||
title: Introducing APISIX's testing framework
|
||||
---
|
||||
|
||||
<!--
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
-->
|
||||
|
||||
APISIX uses a testing framework based on test-nginx: https://github.com/openresty/test-nginx.
|
||||
For details, you can check the [documentation](https://metacpan.org/pod/Test::Nginx) of this project.
|
||||
|
||||
If you want to test the CLI behavior of APISIX (`./bin/apisix`),
|
||||
you need to write a shell script in the t/cli directory to test it. You can refer to the existing test scripts for more details.
|
||||
|
||||
If you want to test the others, you need to write test code based on the framework.
|
||||
|
||||
Here, we briefly describe how to do simple testing based on this framework.
|
||||
|
||||
## Test file
|
||||
|
||||
you need to write test cases in the t/ directory, in a corresponding `.t` file. Note that a single test file should not exceed `800` lines, and if it is too long, it needs to be divided by a suffix. For example:
|
||||
|
||||
```
|
||||
t/
|
||||
├── admin
|
||||
│ ├── consumers.t
|
||||
│ ├── consumers2.t
|
||||
```
|
||||
|
||||
Both `consumers.t` and `consumers2.t` contain tests for consumers in the Admin API.
|
||||
|
||||
Some of the test files start with this paragraph:
|
||||
|
||||
```
|
||||
add_block_preprocessor(sub {
|
||||
my ($block) = @_;
|
||||
|
||||
if (! $block->request) {
|
||||
$block->set_value("request", "GET /t");
|
||||
}
|
||||
|
||||
if (! $block->no_error_log && ! $block->error_log) {
|
||||
$block->set_value("no_error_log", "[error]\n[alert]");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
It means that all tests in this test file that do not define `request` are set to `GET /t`. The same is true for error_log.
|
||||
|
||||
## Preparing the configuration
|
||||
|
||||
When testing a behavior, we need to prepare the configuration.
|
||||
|
||||
If the configuration is from etcd:
|
||||
We can set up specific configurations through the Admin API.
|
||||
|
||||
```
|
||||
=== TEST 7: refer to empty nodes upstream
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local core = require("apisix.core")
|
||||
local t = require("lib.test_admin").test
|
||||
local code, message = t('/apisix/admin/routes/1',
|
||||
ngx.HTTP_PUT,
|
||||
[[{
|
||||
"methods": ["GET"],
|
||||
"upstream_id": "1",
|
||||
"uri": "/index.html"
|
||||
}]]
|
||||
)
|
||||
|
||||
if code >= 300 then
|
||||
ngx.status = code
|
||||
ngx.print(message)
|
||||
return
|
||||
end
|
||||
|
||||
ngx.say(message)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /t
|
||||
--- response_body
|
||||
passed
|
||||
```
|
||||
|
||||
Then trigger it in a later test:
|
||||
|
||||
```
|
||||
=== TEST 8: hit empty nodes upstream
|
||||
--- request
|
||||
GET /index.html
|
||||
--- error_code: 503
|
||||
--- error_log
|
||||
no valid upstream node
|
||||
```
|
||||
|
||||
## Preparing the upstream
|
||||
|
||||
To test the code, we need to provide a mock upstream.
|
||||
|
||||
For HTTP request, the upstream code is put in `t/lib/server.lua`. HTTP request with
|
||||
a given `path` will trigger the method in the same name. For example, a call to `/server_port`
|
||||
will call the `_M.server_port`.
|
||||
|
||||
For TCP request, a dummy upstream is used:
|
||||
|
||||
```
|
||||
local sock = ngx.req.socket()
|
||||
local data = sock:receive("1")
|
||||
ngx.say("hello world")
|
||||
```
|
||||
|
||||
If you want to custom the TCP upstream logic, you can use:
|
||||
|
||||
```
|
||||
--- stream_upstream_code
|
||||
local sock = ngx.req.socket()
|
||||
local data = sock:receive("1")
|
||||
ngx.sleep(0.2)
|
||||
ngx.say("hello world")
|
||||
```
|
||||
|
||||
## Send request
|
||||
|
||||
We can initiate a request with `request` and set the request headers with `more_headers`.
|
||||
|
||||
For example.
|
||||
|
||||
```
|
||||
--- request
|
||||
PUT /hello?xx=y&xx=z&&y=&&z
|
||||
body part of the request
|
||||
--- more_headers
|
||||
X-Req: foo
|
||||
X-Req: bar
|
||||
X-Resp: cat
|
||||
```
|
||||
|
||||
Lua code can be used to send multiple requests.
|
||||
|
||||
One request after another:
|
||||
|
||||
```
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local http = require "resty.http"
|
||||
local uri = "http://127.0.0.1:" .. ngx.var.server_port
|
||||
.. "/server_port"
|
||||
|
||||
local ports_count = {}
|
||||
for i = 1, 12 do
|
||||
local httpc = http.new()
|
||||
local res, err = httpc:request_uri(uri, {method = "GET"})
|
||||
if not res then
|
||||
ngx.say(err)
|
||||
return
|
||||
end
|
||||
ports_count[res.body] = (ports_count[res.body] or 0) + 1
|
||||
end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Sending multiple requests concurrently:
|
||||
|
||||
```
|
||||
--- config
|
||||
location /t {
|
||||
content_by_lua_block {
|
||||
local http = require "resty.http"
|
||||
local uri = "http://127.0.0.1:" .. ngx.var.server_port
|
||||
.. "/server_port?var=2&var2="
|
||||
|
||||
|
||||
local t = {}
|
||||
local ports_count = {}
|
||||
for i = 1, 180 do
|
||||
local th = assert(ngx.thread.spawn(function(i)
|
||||
local httpc = http.new()
|
||||
local res, err = httpc:request_uri(uri..i, {method = "GET"})
|
||||
if not res then
|
||||
ngx.log(ngx.ERR, err)
|
||||
return
|
||||
end
|
||||
ports_count[res.body] = (ports_count[res.body] or 0) + 1
|
||||
end, i))
|
||||
table.insert(t, th)
|
||||
end
|
||||
for i, th in ipairs(t) do
|
||||
ngx.thread.wait(th)
|
||||
end
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Send TCP request
|
||||
|
||||
We can use `stream_request` to send a TCP request, for example:
|
||||
|
||||
```
|
||||
--- stream_request
|
||||
hello
|
||||
```
|
||||
|
||||
To send a TLS over TCP request, we can combine `stream_tls_request` with `stream_sni`:
|
||||
|
||||
```
|
||||
--- stream_tls_request
|
||||
mmm
|
||||
--- stream_sni: xx.com
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
The following assertions are commonly used.
|
||||
|
||||
Check status (if not set, the framework will check if the request has 200 status code).
|
||||
|
||||
```
|
||||
--- error_code: 405
|
||||
```
|
||||
|
||||
Check response headers.
|
||||
|
||||
```
|
||||
--- response_headers
|
||||
X-Resp: foo
|
||||
X-Req: foo, bar
|
||||
```
|
||||
|
||||
Check response body.
|
||||
|
||||
```
|
||||
--- response_body
|
||||
[{"count":12, "port": "1982"}]
|
||||
```
|
||||
|
||||
Check the TCP response.
|
||||
|
||||
When the request is sent via `stream_request`:
|
||||
|
||||
```
|
||||
--- stream_response
|
||||
receive stream response error: connection reset by peer
|
||||
```
|
||||
|
||||
When the request is sent via `stream_tls_request`:
|
||||
|
||||
```
|
||||
--- response_body
|
||||
receive stream response error: connection reset by peer
|
||||
```
|
||||
|
||||
Checking the error log (via grep error log with regular expression).
|
||||
|
||||
```
|
||||
--- grep_error_log eval
|
||||
qr/hash_on: header|chash_key: "custom-one"/
|
||||
--- grep_error_log_out
|
||||
hash_on: header
|
||||
chash_key: "custom-one"
|
||||
hash_on: header
|
||||
chash_key: "custom-one"
|
||||
hash_on: header
|
||||
chash_key: "custom-one"
|
||||
hash_on: header
|
||||
chash_key: "custom-one"
|
||||
```
|
||||
|
||||
The default log level is `info`, but you can get the debug level log with `--- log_level: debug`.
|
||||
|
||||
## Upstream
|
||||
|
||||
The test framework listens to multiple ports when it is started.
|
||||
|
||||
* 1980/1981/1982/5044: HTTP upstream port. We provide a mock upstream server for testing. See below for more information.
|
||||
* 1983: HTTPS upstream port
|
||||
* 1984: APISIX HTTP port. Can be used to verify HTTP related gateway logic, such as concurrent access to an API.
|
||||
* 1985: APISIX TCP port. Can be used to verify TCP related gateway logic, such as concurrent access to an API.
|
||||
* 1994: APISIX HTTPS port. Can be used to verify HTTPS related gateway logic, such as testing certificate matching logic.
|
||||
* 1995: TCP upstream port
|
||||
* 2005: APISIX TLS over TCP port. Can be used to verify TLS over TCP related gateway logic, such as concurrent access to an API.
|
||||
|
||||
The methods in `t/lib/server.lua` are executed when accessing the upstream port. `_M.go` is the entry point for this file.
|
||||
When the request accesses the upstream `/xxx`, the `_M.xxx` method is executed. For example, a request for `/hello` will execute `_M.hello`.
|
||||
This allows us to write methods inside `t/lib/server.lua` to emulate specific upstream logic, such as sending special responses.
|
||||
|
||||
Note that before adding new methods to `t/lib/server.lua`, make sure that you can reuse existing methods.
|
||||
|
||||
## Run the test
|
||||
|
||||
Assume your current work directory is the root of the apisix source code.
|
||||
|
||||
1. Git clone the latest [test-nginx](https://github.com/openresty/test-nginx) to `../test-nginx`.
|
||||
2. Run the test: `prove -I. -I../test-nginx/inc -I../test-nginx/lib -r t/path/to/file.t`.
|
||||
|
||||
## Tips
|
||||
|
||||
### Debugging test cases
|
||||
|
||||
The Nginx configuration and logs generated by the test cases are located in the t/servroot directory. The Nginx configuration template for testing is located in t/APISIX.pm.
|
||||
|
||||
### Running only some test cases
|
||||
|
||||
Three notes can be used to control which parts of the tests are executed.
|
||||
|
||||
FIRST & LAST:
|
||||
|
||||
```
|
||||
=== TEST 1: vars rule with ! (set)
|
||||
--- FIRST
|
||||
--- config
|
||||
...
|
||||
--- response_body
|
||||
passed
|
||||
|
||||
|
||||
|
||||
=== TEST 2: vars rule with ! (hit)
|
||||
--- request
|
||||
GET /hello?name=jack&age=17
|
||||
--- LAST
|
||||
--- error_code: 403
|
||||
--- response_body
|
||||
Fault Injection!
|
||||
```
|
||||
|
||||
ONLY:
|
||||
|
||||
```
|
||||
=== TEST 1: list empty resources
|
||||
--- ONLY
|
||||
--- config
|
||||
...
|
||||
--- response_body
|
||||
{"count":0,"node":{"dir":true,"key":"/apisix/upstreams","nodes":[]}}
|
||||
```
|
||||
|
||||
### Executing Shell Commands
|
||||
|
||||
It is possible to execute shell commands while writing tests in test-nginx for APISIX. We expose this feature via `exec` code block. The `stdout` of the executed process can be captured via `response_body` code block and `stderr` (if any) can be captured by filtering error.log through `grep_error_log`. Here is an example:
|
||||
|
||||
```
|
||||
=== TEST 1: check exec stdout
|
||||
--- exec
|
||||
echo hello world
|
||||
--- response_body
|
||||
hello world
|
||||
|
||||
|
||||
=== TEST 2: when exec returns an error
|
||||
--- exec
|
||||
echxo hello world
|
||||
--- grep_error_log eval
|
||||
qr/failed to execute the script [ -~]*/
|
||||
--- grep_error_log_out
|
||||
failed to execute the script with status: 127, reason: exit, stderr: /bin/sh: 1: echxo: not found
|
||||
```
|
Reference in New Issue
Block a user