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:
2025-09-04 09:42:47 -05:00
parent f7bae09f22
commit 54cc5f7308
1608 changed files with 388342 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
--
-- 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 core = require("apisix.core")
local sdk = require("apisix.stream.xrpc.sdk")
local xrpc_socket = require("resty.apisix.stream.xrpc.socket")
local math_random = math.random
local ngx = ngx
local OK = ngx.OK
local str_format = string.format
local DECLINED = ngx.DECLINED
local DONE = ngx.DONE
local bit = require("bit")
local ffi = require("ffi")
local ffi_str = ffi.string
-- dubbo protocol spec: https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/tcp/
local header_len = 16
local _M = {}
function _M.init_downstream(session)
session.req_id_seq = 0
session.resp_id_seq = 0
session.cmd_labels = { session.route.id, "" }
return xrpc_socket.downstream.socket()
end
local function parse_dubbo_header(header)
for i = 1, header_len do
local currentByte = header:byte(i)
if not currentByte then
return nil
end
end
local magic_number = str_format("%04x", header:byte(1) * 256 + header:byte(2))
local message_flag = header:byte(3)
local status = header:byte(4)
local request_id = 0
for i = 5, 12 do
request_id = request_id * 256 + header:byte(i)
end
local byte13Val = header:byte(13) * 256 * 256 * 256
local byte14Val = header:byte(14) * 256 * 256
local data_length = byte13Val + byte14Val + header:byte(15) * 256 + header:byte(16)
local is_request = bit.band(bit.rshift(message_flag, 7), 0x01) == 1 and 1 or 0
local is_two_way = bit.band(bit.rshift(message_flag, 6), 0x01) == 1 and 1 or 0
local is_event = bit.band(bit.rshift(message_flag, 5), 0x01) == 1 and 1 or 0
return {
magic_number = magic_number,
message_flag = message_flag,
is_request = is_request,
is_two_way = is_two_way,
is_event = is_event,
status = status,
request_id = request_id,
data_length = data_length
}
end
local function read_data(sk, is_req)
local header_data, err = sk:read(header_len)
if not header_data then
return nil, err, false
end
local header_str = ffi_str(header_data, header_len)
local header_info = parse_dubbo_header(header_str)
if not header_info then
return nil, "header insufficient", false
end
local is_valid_magic_number = header_info.magic_number == "dabb"
if not is_valid_magic_number then
return nil, str_format("unknown magic number: \"%s\"", header_info.magic_number), false
end
local body_data, err = sk:read(header_info.data_length)
if not body_data then
core.log.error("failed to read dubbo request body")
return nil, err, false
end
local ctx = ngx.ctx
ctx.dubbo_serialization_id = bit.band(header_info.message_flag, 0x1F)
if is_req then
ctx.dubbo_req_body_data = body_data
else
ctx.dubbo_rsp_body_data = body_data
end
return true, nil, false
end
local function read_req(sk)
return read_data(sk, true)
end
local function read_reply(sk)
return read_data(sk, false)
end
local function handle_reply(session, sk)
local ok, err = read_reply(sk)
if not ok then
return nil, err
end
local ctx = sdk.get_req_ctx(session, 10)
return ctx
end
function _M.from_downstream(session, downstream)
local read_pipeline = false
session.req_id_seq = session.req_id_seq + 1
local ctx = sdk.get_req_ctx(session, session.req_id_seq)
session._downstream_ctx = ctx
while true do
local ok, err, pipelined = read_req(downstream)
if not ok then
if err ~= "timeout" and err ~= "closed" then
core.log.error("failed to read request: ", err)
end
if read_pipeline and err == "timeout" then
break
end
return DECLINED
end
if not pipelined then
break
end
if not read_pipeline then
read_pipeline = true
-- set minimal read timeout to read pipelined data
downstream:settimeouts(0, 0, 1)
end
end
if read_pipeline then
-- set timeout back
downstream:settimeouts(0, 0, 0)
end
return OK, ctx
end
function _M.connect_upstream(session, ctx)
local conf = session.upstream_conf
local nodes = conf.nodes
if #nodes == 0 then
core.log.error("failed to connect: no nodes")
return DECLINED
end
local node = nodes[math_random(#nodes)]
local sk = sdk.connect_upstream(node, conf)
if not sk then
return DECLINED
end
core.log.debug("dubbo_connect_upstream end")
return OK, sk
end
function _M.disconnect_upstream(session, upstream)
sdk.disconnect_upstream(upstream, session.upstream_conf)
end
function _M.to_upstream(session, ctx, downstream, upstream)
local ok, _ = upstream:move(downstream)
if not ok then
return DECLINED
end
return OK
end
function _M.from_upstream(session, downstream, upstream)
local ctx,err = handle_reply(session, upstream)
if err then
return DECLINED
end
local ok, _ = downstream:move(upstream)
if not ok then
return DECLINED
end
return DONE, ctx
end
function _M.log(_, _)
end
return _M

View File

@@ -0,0 +1,32 @@
--
-- 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 core = require("apisix.core")
local schema = {
type = "object",
}
local _M = {}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
return _M

View File

@@ -0,0 +1,222 @@
--
-- 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 ipairs = ipairs
local pairs = pairs
local cmd_to_key_finder = {}
--[[
-- the data is generated from the script below
local redis = require "resty.redis"
local red = redis:new()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res = red:command("info")
local map = {}
for _, r in ipairs(res) do
local first_key = r[4]
local last_key = r[5]
local step = r[6]
local idx = first_key .. ':' .. last_key .. ':' .. step
if idx ~= "1:1:1" then
-- "1:1:1" is the default
if map[idx] then
table.insert(map[idx], r[1])
else
map[idx] = {r[1]}
end
end
end
for _, r in pairs(map) do
table.sort(r)
end
local dump = require('pl.pretty').dump; dump(map)
--]]
local key_to_cmd = {
["0:0:0"] = {
"acl",
"asking",
"auth",
"bgrewriteaof",
"bgsave",
"blmpop",
"bzmpop",
"client",
"cluster",
"command",
"config",
"dbsize",
"debug",
"discard",
"echo",
"eval",
"eval_ro",
"evalsha",
"evalsha_ro",
"exec",
"failover",
"fcall",
"fcall_ro",
"flushall",
"flushdb",
"function",
"hello",
"info",
"keys",
"lastsave",
"latency",
"lmpop",
"lolwut",
"memory",
"module",
"monitor",
"multi",
"object",
"pfselftest",
"ping",
"psubscribe",
"psync",
"publish",
"pubsub",
"punsubscribe",
"quit",
"randomkey",
"readonly",
"readwrite",
"replconf",
"replicaof",
"reset",
"role",
"save",
"scan",
"script",
"select",
"shutdown",
"sintercard",
"slaveof",
"slowlog",
"subscribe",
"swapdb",
"sync",
"time",
"unsubscribe",
"unwatch",
"wait",
"xgroup",
"xinfo",
"xread",
"xreadgroup",
"zdiff",
"zinter",
"zintercard",
"zmpop",
"zunion"
},
["1:-1:1"] = {
"del",
"exists",
"mget",
"pfcount",
"pfmerge",
"sdiff",
"sdiffstore",
"sinter",
"sinterstore",
"ssubscribe",
"sunion",
"sunionstore",
"sunsubscribe",
"touch",
"unlink",
"watch"
},
["1:-1:2"] = {
"mset",
"msetnx"
},
["1:-2:1"] = {
"blpop",
"brpop",
"bzpopmax",
"bzpopmin"
},
["1:2:1"] = {
"blmove",
"brpoplpush",
"copy",
"geosearchstore",
"lcs",
"lmove",
"rename",
"renamenx",
"rpoplpush",
"smove",
"zrangestore"
},
["2:-1:1"] = {
"bitop"
},
["2:2:1"] = {
"pfdebug"
},
["3:3:1"] = {
"migrate"
}
}
local key_finders = {
["0:0:0"] = false,
["1:-1:1"] = function (idx, narg)
return 1 < idx
end,
["1:-1:2"] = function (idx, narg)
return 1 < idx and idx % 2 == 0
end,
["1:-2:1"] = function (idx, narg)
return 1 < idx and idx < narg - 1
end,
["1:2:1"] = function (idx, narg)
return idx == 2 or idx == 3
end,
["2:-1:1"] = function (idx, narg)
return 2 < idx
end,
["2:2:1"] = function (idx, narg)
return idx == 3
end,
["3:3:1"] = function (idx, narg)
return idx == 4
end
}
for k, cmds in pairs(key_to_cmd) do
for _, cmd in ipairs(cmds) do
cmd_to_key_finder[cmd] = key_finders[k]
end
end
return {
cmd_to_key_finder = cmd_to_key_finder,
default_key_finder = function (idx, narg)
return idx == 2
end,
}

View File

@@ -0,0 +1,499 @@
--
-- 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 core = require("apisix.core")
local sdk = require("apisix.stream.xrpc.sdk")
local commands = require("apisix.stream.xrpc.protocols.redis.commands")
local xrpc_socket = require("resty.apisix.stream.xrpc.socket")
local ffi = require("ffi")
local ffi_str = ffi.string
local math_random = math.random
local OK = ngx.OK
local DECLINED = ngx.DECLINED
local DONE = ngx.DONE
local sleep = ngx.sleep
local str_byte = string.byte
local str_fmt = string.format
local ipairs = ipairs
local tonumber = tonumber
-- this variable is only used to log the redis command line in log_format
-- and is not used for filter in the logger phase.
core.ctx.register_var("redis_cmd_line", function(ctx)
return core.table.concat(ctx.cmd_line, " ")
end)
-- redis protocol spec: https://redis.io/docs/reference/protocol-spec/
-- There is no plan to support inline command format
local protocol_name = "redis"
local _M = {}
local MAX_LINE_LEN = 128
local MAX_VALUE_LEN = 128
local PREFIX_ARR = str_byte("*")
local PREFIX_STR = str_byte("$")
local PREFIX_STA = str_byte("+")
local PREFIX_INT = str_byte(":")
local PREFIX_ERR = str_byte("-")
local lrucache = core.lrucache.new({
type = "plugin",
})
local function create_matcher(conf)
local matcher = {}
--[[
{"delay": 5, "key":"x", "commands":["GET", "MGET"]}
{"delay": 5, "commands":["GET"]}
=> {
get = {keys = {x = {delay = 5}, * = {delay = 5}}}
mget = {keys = {x = {delay = 5}}}
}
]]--
for _, rule in ipairs(conf.faults) do
for _, cmd in ipairs(rule.commands) do
cmd = cmd:lower()
local key = rule.key
local kf = commands.cmd_to_key_finder[cmd]
local key_matcher = matcher[cmd]
if not key_matcher then
key_matcher = {
keys = {}
}
matcher[cmd] = key_matcher
end
if not key or kf == false then
key = "*"
end
if key_matcher.keys[key] then
core.log.warn("override existent fault rule of cmd: ", cmd, ", key: ", key)
end
key_matcher.keys[key] = rule
end
end
return matcher
end
local function get_matcher(conf, ctx)
return core.lrucache.plugin_ctx(lrucache, ctx, nil, create_matcher, conf)
end
function _M.init_downstream(session)
local conf = session.route.protocol.conf
if conf and conf.faults then
local matcher = get_matcher(conf, session.conn_ctx)
session.matcher = matcher
end
session.req_id_seq = 0
session.resp_id_seq = 0
session.cmd_labels = {session.route.id, ""}
return xrpc_socket.downstream.socket()
end
local function read_line(sk)
local p, err, len = sk:read_line(MAX_LINE_LEN)
if not p then
return nil, err
end
if len < 2 then
return nil, "line too short"
end
return p, nil, len
end
local function read_len(sk)
local p, err, len = read_line(sk)
if not p then
return nil, err
end
local s = ffi_str(p + 1, len - 1)
local n = tonumber(s)
if not n then
return nil, str_fmt("invalid len string: \"%s\"", s)
end
return n
end
local function read_req(session, sk)
local narg, err = read_len(sk)
if not narg then
return nil, err
end
local cmd_line = core.tablepool.fetch("xrpc_redis_cmd_line", narg, 0)
local n, err = read_len(sk)
if not n then
return nil, err
end
local p, err = sk:read(n + 2)
if not p then
return nil, err
end
local s = ffi_str(p, n)
local cmd = s:lower()
cmd_line[1] = cmd
if cmd == "subscribe" or cmd == "psubscribe" then
session.in_pub_sub = true
end
local key_finder
local matcher = session.matcher
if matcher then
matcher = matcher[s:lower()]
if matcher then
key_finder = commands.cmd_to_key_finder[s] or commands.default_key_finder
end
end
for i = 2, narg do
local is_key = false
if key_finder then
is_key = key_finder(i, narg)
end
local n, err = read_len(sk)
if not n then
return nil, err
end
local s
if not is_key and n > MAX_VALUE_LEN then
-- avoid recording big value
local p, err = sk:read(MAX_VALUE_LEN)
if not p then
return nil, err
end
local ok, err = sk:drain(n - MAX_VALUE_LEN + 2)
if not ok then
return nil, err
end
s = ffi_str(p, MAX_VALUE_LEN) .. "...(" .. n .. " bytes)"
else
local p, err = sk:read(n + 2)
if not p then
return nil, err
end
s = ffi_str(p, n)
if is_key and matcher.keys[s] then
matcher = matcher.keys[s]
key_finder = nil
end
end
cmd_line[i] = s
end
session.req_id_seq = session.req_id_seq + 1
local ctx = sdk.get_req_ctx(session, session.req_id_seq)
ctx.cmd_line = cmd_line
ctx.cmd = cmd
local pipelined = sk:has_pending_data()
if matcher then
if matcher.keys then
-- try to match any key of this command
matcher = matcher.keys["*"]
end
if matcher then
sleep(matcher.delay)
end
end
return true, nil, pipelined
end
local function read_subscribe_reply(sk)
local line, err, n = read_line(sk)
if not line then
return nil, err
end
local prefix = line[0]
if prefix == PREFIX_STR then -- char '$'
local size = tonumber(ffi_str(line + 1, n - 1))
if size < 0 then
return true
end
local p, err = sk:read(size + 2)
if not p then
return nil, err
end
return ffi_str(p, size)
elseif prefix == PREFIX_INT then -- char ':'
return tonumber(ffi_str(line + 1, n - 1))
else
return nil, str_fmt("unknown prefix: \"%s\"", prefix)
end
end
local function read_reply(sk, session)
local line, err, n = read_line(sk)
if not line then
return nil, err
end
local prefix = line[0]
if prefix == PREFIX_STR then -- char '$'
-- print("bulk reply")
local size = tonumber(ffi_str(line + 1, n - 1))
if size < 0 then
return true
end
local ok, err = sk:drain(size + 2)
if not ok then
return nil, err
end
return true
elseif prefix == PREFIX_STA then -- char '+'
-- print("status reply")
return true
elseif prefix == PREFIX_ARR then -- char '*'
local narr = tonumber(ffi_str(line + 1, n - 1))
-- print("multi-bulk reply: ", narr)
if narr < 0 then
return true
end
if session and session.in_pub_sub and (narr == 3 or narr == 4) then
local msg_type, err = read_subscribe_reply(sk)
if msg_type == nil then
return nil, err
end
session.pub_sub_msg_type = msg_type
local res, err = read_reply(sk)
if res == nil then
return nil, err
end
if msg_type == "unsubscribe" or msg_type == "punsubscribe" then
local n_ch, err = read_subscribe_reply(sk)
if n_ch == nil then
return nil, err
end
if n_ch == 0 then
session.in_pub_sub = -1
-- clear this flag later at the end of `handle_reply`
end
else
local n = msg_type == "pmessage" and 2 or 1
for i = 1, n do
local res, err = read_reply(sk)
if res == nil then
return nil, err
end
end
end
else
for i = 1, narr do
local res, err = read_reply(sk)
if res == nil then
return nil, err
end
end
end
return true
elseif prefix == PREFIX_INT then -- char ':'
-- print("integer reply")
return true
elseif prefix == PREFIX_ERR then -- char '-'
-- print("error reply: ", n)
return true
else
return nil, str_fmt("unknown prefix: \"%s\"", prefix)
end
end
local function handle_reply(session, sk)
local ok, err = read_reply(sk, session)
if not ok then
return nil, err
end
local ctx
if session.in_pub_sub and session.pub_sub_msg_type then
local msg_type = session.pub_sub_msg_type
session.pub_sub_msg_type = nil
if session.resp_id_seq < session.req_id_seq then
local cur_ctx = sdk.get_req_ctx(session, session.resp_id_seq + 1)
local cmd = cur_ctx.cmd
if cmd == msg_type then
ctx = cur_ctx
session.resp_id_seq = session.resp_id_seq + 1
end
end
if session.in_pub_sub == -1 then
session.in_pub_sub = nil
end
else
session.resp_id_seq = session.resp_id_seq + 1
ctx = sdk.get_req_ctx(session, session.resp_id_seq)
end
return ctx
end
function _M.from_downstream(session, downstream)
local read_pipeline = false
while true do
local ok, err, pipelined = read_req(session, downstream)
if not ok then
if err ~= "timeout" and err ~= "closed" then
core.log.error("failed to read request: ", err)
end
if read_pipeline and err == "timeout" then
break
end
return DECLINED
end
if not pipelined then
break
end
if not read_pipeline then
read_pipeline = true
-- set minimal read timeout to read pipelined data
downstream:settimeouts(0, 0, 1)
end
end
if read_pipeline then
-- set timeout back
downstream:settimeouts(0, 0, 0)
end
return OK
end
function _M.connect_upstream(session, ctx)
local conf = session.upstream_conf
local nodes = conf.nodes
if #nodes == 0 then
core.log.error("failed to connect: no nodes")
return DECLINED
end
local node = nodes[math_random(#nodes)]
local sk = sdk.connect_upstream(node, conf)
if not sk then
return DECLINED
end
return OK, sk
end
function _M.disconnect_upstream(session, upstream)
sdk.disconnect_upstream(upstream, session.upstream_conf)
end
function _M.to_upstream(session, ctx, downstream, upstream)
local ok, err = upstream:move(downstream)
if not ok then
core.log.error("failed to send to upstream: ", err)
return DECLINED
end
return OK
end
function _M.from_upstream(session, downstream, upstream)
local ctx, err = handle_reply(session, upstream)
if err then
core.log.error("failed to handle upstream: ", err)
return DECLINED
end
local ok, err = downstream:move(upstream)
if not ok then
core.log.error("failed to handle upstream: ", err)
return DECLINED
end
return DONE, ctx
end
function _M.log(session, ctx)
local metrics = sdk.get_metrics(session, protocol_name)
if metrics then
session.cmd_labels[2] = ctx.cmd
metrics.commands_total:inc(1, session.cmd_labels)
metrics.commands_latency_seconds:observe(ctx.var.rpc_time, session.cmd_labels)
end
core.tablepool.release("xrpc_redis_cmd_line", ctx.cmd_line)
ctx.cmd_line = nil
end
return _M

View File

@@ -0,0 +1,33 @@
--
-- 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 _M = {
commands_total = {
type = "counter",
help = "Total number of requests for a specific Redis command",
labels = {"route", "command"},
},
commands_latency_seconds = {
type = "histogram",
help = "Latency of requests for a specific Redis command",
labels = {"route", "command"},
-- latency buckets, 1ms to 1s:
buckets = {0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1}
},
}
return _M

View File

@@ -0,0 +1,59 @@
--
-- 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 core = require("apisix.core")
local schema = {
type = "object",
properties = {
faults = {
type = "array",
minItems = 1,
items = {
type = "object",
properties = {
commands = {
type = "array",
minItems = 1,
items = {
type = "string"
},
},
key = {
type = "string",
minLength = 1,
},
delay = {
type = "number",
description = "additional delay in seconds",
}
},
required = {"commands", "delay"}
},
},
},
}
local _M = {}
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
return _M