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,212 @@
--
-- 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 require = require
local router = require("apisix.utils.router")
local radixtree = require("resty.radixtree")
local builtin_v1_routes = require("apisix.control.v1")
local plugin_mod = require("apisix.plugin")
local core = require("apisix.core")
local str_sub = string.sub
local ipairs = ipairs
local pairs = pairs
local type = type
local ngx = ngx
local get_method = ngx.req.get_method
local events = require("apisix.events")
local _M = {}
local function format_dismod_uri(mod_name, uri)
if core.string.has_prefix(uri, "/v1/") then
return uri
end
local tmp = {"/v1/discovery/", mod_name}
if not core.string.has_prefix(uri, "/") then
core.table.insert(tmp, "/")
end
core.table.insert(tmp, uri)
return core.table.concat(tmp, "")
end
-- we do not hardcode the discovery module's control api uri
local function format_dismod_control_api_uris(mod_name, api_route)
if not api_route or #api_route == 0 then
return api_route
end
local clone_route = core.table.clone(api_route)
for _, v in ipairs(clone_route) do
local uris = v.uris
local target_uris = core.table.new(#uris, 0)
for _, uri in ipairs(uris) do
local target_uri = format_dismod_uri(mod_name, uri)
core.table.insert(target_uris, target_uri)
end
v.uris = target_uris
end
return clone_route
end
local fetch_control_api_router
do
local function register_api_routes(routes, api_routes)
for _, route in ipairs(api_routes) do
core.table.insert(routes, {
methods = route.methods,
-- note that it is 'uris' for control API, which is an array of strings
paths = route.uris,
handler = function (api_ctx)
local code, body = route.handler(api_ctx)
if code or body then
if type(body) == "table" and ngx.header["Content-Type"] == nil then
core.response.set_header("Content-Type", "application/json")
end
core.response.exit(code, body)
end
end
})
end
end
local routes = {}
local v1_routes = {}
local function empty_func() end
function fetch_control_api_router()
core.table.clear(routes)
for _, plugin in ipairs(plugin_mod.plugins) do
local api_fun = plugin.control_api
if api_fun then
local api_route = api_fun()
register_api_routes(routes, api_route)
end
end
local discovery_type = require("apisix.core.config_local").local_conf().discovery
if discovery_type then
local discovery = require("apisix.discovery.init").discovery
local dump_apis = {}
for key, _ in pairs(discovery_type) do
local dis_mod = discovery[key]
-- if discovery module has control_api method, support it
local api_fun = dis_mod.control_api
if api_fun then
local api_route = api_fun()
local format_route = format_dismod_control_api_uris(key, api_route)
register_api_routes(routes, format_route)
end
local dump_data = dis_mod.dump_data
if dump_data then
local target_uri = format_dismod_uri(key, "/dump")
local item = {
methods = {"GET"},
uris = {target_uri},
handler = function()
return 200, dump_data()
end
}
core.table.insert(dump_apis, item)
end
end
if #dump_apis > 0 then
core.log.notice("dump_apis: ", core.json.encode(dump_apis, true))
register_api_routes(routes, dump_apis)
end
end
core.table.clear(v1_routes)
register_api_routes(v1_routes, builtin_v1_routes)
local v1_router, err = router.new(v1_routes)
if not v1_router then
return nil, err
end
core.table.insert(routes, {
paths = {"/v1/*"},
filter_fun = function(vars, opts, ...)
local uri = str_sub(vars.uri, #"/v1" + 1)
return v1_router:dispatch(uri, opts, ...)
end,
handler = empty_func,
})
local with_parameter = false
local conf = core.config.local_conf()
if conf.apisix.enable_control and conf.apisix.control then
if conf.apisix.control.router == "radixtree_uri_with_parameter" then
with_parameter = true
end
end
if with_parameter then
return radixtree.new(routes)
else
return router.new(routes)
end
end
end -- do
do
local match_opts = {}
local cached_version
local router
function _M.match(uri)
if cached_version ~= plugin_mod.load_times then
local err
router, err = fetch_control_api_router()
if router == nil then
core.log.error("failed to fetch valid api router: ", err)
return false
end
cached_version = plugin_mod.load_times
end
core.table.clear(match_opts)
match_opts.method = get_method()
return router:dispatch(uri, match_opts)
end
end -- do
local function reload_plugins()
core.log.info("start to hot reload plugins")
plugin_mod.load()
end
function _M.init_worker()
-- register reload plugin handler
events:register(reload_plugins, builtin_v1_routes.reload_event, "PUT")
end
return _M

View File

@@ -0,0 +1,506 @@
--
-- 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 require = require
local core = require("apisix.core")
local plugin = require("apisix.plugin")
local get_routes = require("apisix.router").http_routes
local get_services = require("apisix.http.service").services
local upstream_mod = require("apisix.upstream")
local get_upstreams = upstream_mod.upstreams
local collectgarbage = collectgarbage
local ipairs = ipairs
local pcall = pcall
local str_format = string.format
local ngx = ngx
local ngx_var = ngx.var
local events = require("apisix.events")
local _M = {}
_M.RELOAD_EVENT = 'control-api-plugin-reload'
function _M.schema()
local http_plugins, stream_plugins = plugin.get_all({
version = true,
priority = true,
schema = true,
metadata_schema = true,
consumer_schema = true,
type = true,
scope = true,
})
local schema = {
main = {
consumer = core.schema.consumer,
consumer_group = core.schema.consumer_group,
global_rule = core.schema.global_rule,
plugin_config = core.schema.plugin_config,
plugins = core.schema.plugins,
proto = core.schema.proto,
route = core.schema.route,
service = core.schema.service,
ssl = core.schema.ssl,
stream_route = core.schema.stream_route,
upstream = core.schema.upstream,
upstream_hash_header_schema = core.schema.upstream_hash_header_schema,
upstream_hash_vars_schema = core.schema.upstream_hash_vars_schema,
},
plugins = http_plugins,
stream_plugins = stream_plugins,
}
return 200, schema
end
local healthcheck
local function extra_checker_info(value)
if not healthcheck then
healthcheck = require("resty.healthcheck")
end
local name = upstream_mod.get_healthchecker_name(value)
local nodes, err = healthcheck.get_target_list(name, "upstream-healthcheck")
if err then
core.log.error("healthcheck.get_target_list failed: ", err)
end
return {
name = value.key,
nodes = nodes,
}
end
local function get_checker_type(checks)
if checks.active and checks.active.type then
return checks.active.type
elseif checks.passive and checks.passive.type then
return checks.passive.type
end
end
local function iter_and_add_healthcheck_info(infos, values)
if not values then
return
end
for _, value in core.config_util.iterate_values(values) do
local checks = value.value.checks or (value.value.upstream and value.value.upstream.checks)
if checks then
local info = extra_checker_info(value)
info.type = get_checker_type(checks)
core.table.insert(infos, info)
end
end
end
local HTML_TEMPLATE = [[
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>APISIX upstream check status</title>
</head>
<body>
<h1>APISIX upstream check status</h1>
<table style="background-color:white" cellspacing="0" cellpadding="3" border="1">
<tr bgcolor="#C0C0C0">
<th>Index</th>
<th>Upstream</th>
<th>Check type</th>
<th>Host</th>
<th>Status</th>
<th>Success counts</th>
<th>TCP Failures</th>
<th>HTTP Failures</th>
<th>TIMEOUT Failures</th>
</tr>
{% local i = 0 %}
{% for _, stat in ipairs(stats) do %}
{% for _, node in ipairs(stat.nodes) do %}
{% i = i + 1 %}
{% if node.status == "healthy" or node.status == "mostly_healthy" then %}
<tr>
{% else %}
<tr bgcolor="#FF0000">
{% end %}
<td>{* i *}</td>
<td>{* stat.name *}</td>
<td>{* stat.type *}</td>
<td>{* node.ip .. ":" .. node.port *}</td>
<td>{* node.status *}</td>
<td>{* node.counter.success *}</td>
<td>{* node.counter.tcp_failure *}</td>
<td>{* node.counter.http_failure *}</td>
<td>{* node.counter.timeout_failure *}</td>
</tr>
{% end %}
{% end %}
</table>
</body>
</html>
]]
local html_render
local function try_render_html(data)
if not html_render then
local template = require("resty.template")
html_render = template.compile(HTML_TEMPLATE)
end
local accept = ngx_var.http_accept
if accept and accept:find("text/html") then
local ok, out = pcall(html_render, data)
if not ok then
local err = str_format("HTML template rendering: %s", out)
core.log.error(err)
return nil, err
end
return out
end
end
local function _get_health_checkers()
local infos = {}
local routes = get_routes()
iter_and_add_healthcheck_info(infos, routes)
local services = get_services()
iter_and_add_healthcheck_info(infos, services)
local upstreams = get_upstreams()
iter_and_add_healthcheck_info(infos, upstreams)
return infos
end
function _M.get_health_checkers()
local infos = _get_health_checkers()
local out, err = try_render_html({stats=infos})
if out then
core.response.set_header("Content-Type", "text/html")
return 200, out
end
if err then
return 503, {error_msg = err}
end
return 200, infos
end
local function iter_and_find_healthcheck_info(values, src_type, src_id)
if not values then
return nil, str_format("%s[%s] not found", src_type, src_id)
end
for _, value in core.config_util.iterate_values(values) do
if value.value.id == src_id then
local checks = value.value.checks or
(value.value.upstream and value.value.upstream.checks)
if not checks then
return nil, str_format("no checker for %s[%s]", src_type, src_id)
end
local info = extra_checker_info(value)
info.type = get_checker_type(checks)
return info
end
end
return nil, str_format("%s[%s] not found", src_type, src_id)
end
function _M.get_health_checker()
local uri_segs = core.utils.split_uri(ngx_var.uri)
core.log.info("healthcheck uri: ", core.json.delay_encode(uri_segs))
local src_type, src_id = uri_segs[4], uri_segs[5]
if not src_id then
return 404, {error_msg = str_format("missing src id for src type %s", src_type)}
end
local values
if src_type == "routes" then
values = get_routes()
elseif src_type == "services" then
values = get_services()
elseif src_type == "upstreams" then
values = get_upstreams()
else
return 400, {error_msg = str_format("invalid src type %s", src_type)}
end
local info, err = iter_and_find_healthcheck_info(values, src_type, src_id)
if not info then
return 404, {error_msg = err}
end
local out, err = try_render_html({stats={info}})
if out then
core.response.set_header("Content-Type", "text/html")
return 200, out
end
if err then
return 503, {error_msg = err}
end
return 200, info
end
local function iter_add_get_routes_info(values, route_id)
local infos = {}
for _, route in core.config_util.iterate_values(values) do
local new_route = core.table.deepcopy(route)
if new_route.value.upstream and new_route.value.upstream.parent then
new_route.value.upstream.parent = nil
end
-- remove healthcheck info
new_route.checker = nil
new_route.checker_idx = nil
new_route.checker_upstream = nil
new_route.clean_handlers = nil
core.table.insert(infos, new_route)
-- check the route id
if route_id and route.value.id == route_id then
return new_route
end
end
if not route_id then
return infos
end
return nil
end
function _M.dump_all_routes_info()
local routes = get_routes()
local infos = iter_add_get_routes_info(routes, nil)
return 200, infos
end
function _M.dump_route_info()
local routes = get_routes()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local route_id = uri_segs[4]
local route = iter_add_get_routes_info(routes, route_id)
if not route then
return 404, {error_msg = str_format("route[%s] not found", route_id)}
end
return 200, route
end
local function iter_add_get_upstream_info(values, upstream_id)
if not values then
return nil
end
local infos = {}
for _, upstream in core.config_util.iterate_values(values) do
local new_upstream = core.table.deepcopy(upstream)
core.table.insert(infos, new_upstream)
if new_upstream.value and new_upstream.value.parent then
new_upstream.value.parent = nil
end
-- check the upstream id
if upstream_id and upstream.value.id == upstream_id then
return new_upstream
end
end
if not upstream_id then
return infos
end
return nil
end
function _M.dump_all_upstreams_info()
local upstreams = get_upstreams()
local infos = iter_add_get_upstream_info(upstreams, nil)
return 200, infos
end
function _M.dump_upstream_info()
local upstreams = get_upstreams()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local upstream_id = uri_segs[4]
local upstream = iter_add_get_upstream_info(upstreams, upstream_id)
if not upstream then
return 404, {error_msg = str_format("upstream[%s] not found", upstream_id)}
end
return 200, upstream
end
function _M.trigger_gc()
-- TODO: find a way to trigger GC in the stream subsystem
collectgarbage()
return 200
end
local function iter_add_get_services_info(values, svc_id)
local infos = {}
for _, svc in core.config_util.iterate_values(values) do
local new_svc = core.table.deepcopy(svc)
if new_svc.value.upstream and new_svc.value.upstream.parent then
new_svc.value.upstream.parent = nil
end
-- remove healthcheck info
new_svc.checker = nil
new_svc.checker_idx = nil
new_svc.checker_upstream = nil
new_svc.clean_handlers = nil
core.table.insert(infos, new_svc)
-- check the service id
if svc_id and svc.value.id == svc_id then
return new_svc
end
end
if not svc_id then
return infos
end
return nil
end
function _M.dump_all_services_info()
local services = get_services()
local infos = iter_add_get_services_info(services, nil)
return 200, infos
end
function _M.dump_service_info()
local services = get_services()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local svc_id = uri_segs[4]
local info = iter_add_get_services_info(services, svc_id)
if not info then
return 404, {error_msg = str_format("service[%s] not found", svc_id)}
end
return 200, info
end
function _M.dump_all_plugin_metadata()
local names = core.config.local_conf().plugins
local metadatas = core.table.new(0, #names)
for _, name in ipairs(names) do
local metadata = plugin.plugin_metadata(name)
if metadata then
core.table.insert(metadatas, metadata.value)
end
end
return 200, metadatas
end
function _M.dump_plugin_metadata()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local name = uri_segs[4]
local metadata = plugin.plugin_metadata(name)
if not metadata then
return 404, {error_msg = str_format("plugin metadata[%s] not found", name)}
end
return 200, metadata.value
end
function _M.post_reload_plugins()
local success, err = events:post(_M.RELOAD_EVENT, ngx.req.get_method(), ngx.time())
if not success then
core.response.exit(503, err)
end
core.response.exit(200, "done")
end
return {
-- /v1/schema
{
methods = {"GET"},
uris = {"/schema"},
handler = _M.schema,
},
-- /v1/healthcheck
{
methods = {"GET"},
uris = {"/healthcheck"},
handler = _M.get_health_checkers,
},
-- /v1/healthcheck/{src_type}/{src_id}
{
methods = {"GET"},
uris = {"/healthcheck/*"},
handler = _M.get_health_checker,
},
-- /v1/gc
{
methods = {"POST"},
uris = {"/gc"},
handler = _M.trigger_gc,
},
-- /v1/routes
{
methods = {"GET"},
uris = {"/routes"},
handler = _M.dump_all_routes_info,
},
-- /v1/route/*
{
methods = {"GET"},
uris = {"/route/*"},
handler = _M.dump_route_info,
},
-- /v1/services
{
methods = {"GET"},
uris = {"/services"},
handler = _M.dump_all_services_info
},
-- /v1/service/*
{
methods = {"GET"},
uris = {"/service/*"},
handler = _M.dump_service_info
},
-- /v1/upstreams
{
methods = {"GET"},
uris = {"/upstreams"},
handler = _M.dump_all_upstreams_info,
},
-- /v1/upstream/*
{
methods = {"GET"},
uris = {"/upstream/*"},
handler = _M.dump_upstream_info,
},
-- /v1/plugin_metadatas
{
methods = {"GET"},
uris = {"/plugin_metadatas"},
handler = _M.dump_all_plugin_metadata,
},
-- /v1/plugin_metadata/*
{
methods = {"GET"},
uris = {"/plugin_metadata/*"},
handler = _M.dump_plugin_metadata,
},
-- /v1/plugins/reload
{
methods = {"PUT"},
uris = {"/plugins/reload"},
handler = _M.post_reload_plugins,
},
get_health_checkers = _get_health_checkers,
reload_event = _M.RELOAD_EVENT,
}