- 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>
527 lines
15 KiB
Lua
527 lines
15 KiB
Lua
--
|
|
-- 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 get_uri_args = ngx.req.get_uri_args
|
|
local route = require("apisix.utils.router")
|
|
local plugin = require("apisix.plugin")
|
|
local standalone = require("apisix.admin.standalone")
|
|
local v3_adapter = require("apisix.admin.v3_adapter")
|
|
local utils = require("apisix.admin.utils")
|
|
local ngx = ngx
|
|
local get_method = ngx.req.get_method
|
|
local ngx_time = ngx.time
|
|
local ngx_timer_at = ngx.timer.at
|
|
local ngx_worker_id = ngx.worker.id
|
|
local tonumber = tonumber
|
|
local tostring = tostring
|
|
local str_lower = string.lower
|
|
local reload_event = "/apisix/admin/plugins/reload"
|
|
local ipairs = ipairs
|
|
local error = error
|
|
local type = type
|
|
|
|
|
|
local events
|
|
local MAX_REQ_BODY = 1024 * 1024 * 1.5 -- 1.5 MiB
|
|
|
|
|
|
local viewer_methods = {
|
|
get = true,
|
|
}
|
|
|
|
|
|
local resources = {
|
|
routes = require("apisix.admin.routes"),
|
|
services = require("apisix.admin.services"),
|
|
upstreams = require("apisix.admin.upstreams"),
|
|
consumers = require("apisix.admin.consumers"),
|
|
credentials = require("apisix.admin.credentials"),
|
|
schema = require("apisix.admin.schema"),
|
|
ssls = require("apisix.admin.ssl"),
|
|
plugins = require("apisix.admin.plugins"),
|
|
protos = require("apisix.admin.proto"),
|
|
global_rules = require("apisix.admin.global_rules"),
|
|
stream_routes = require("apisix.admin.stream_routes"),
|
|
plugin_metadata = require("apisix.admin.plugin_metadata"),
|
|
plugin_configs = require("apisix.admin.plugin_config"),
|
|
consumer_groups = require("apisix.admin.consumer_group"),
|
|
secrets = require("apisix.admin.secrets"),
|
|
}
|
|
|
|
|
|
local _M = {version = 0.4}
|
|
local router
|
|
|
|
|
|
local function check_token(ctx)
|
|
local local_conf = core.config.local_conf()
|
|
|
|
-- check if admin_key is required
|
|
if local_conf.deployment.admin.admin_key_required == false then
|
|
return true
|
|
end
|
|
|
|
local admin_key = core.table.try_read_attr(local_conf, "deployment", "admin", "admin_key")
|
|
if not admin_key then
|
|
return true
|
|
end
|
|
|
|
local req_token = ctx.var.arg_api_key or ctx.var.http_x_api_key
|
|
or ctx.var.cookie_x_api_key
|
|
if not req_token then
|
|
return false, "missing apikey"
|
|
end
|
|
|
|
local admin
|
|
for i, row in ipairs(admin_key) do
|
|
if req_token == row.key then
|
|
admin = row
|
|
break
|
|
end
|
|
end
|
|
|
|
if not admin then
|
|
return false, "wrong apikey"
|
|
end
|
|
|
|
if admin.role == "viewer" and
|
|
not viewer_methods[str_lower(get_method())] then
|
|
return false, "invalid method for role viewer"
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Set the `apictx` variable and check admin api token, if the check fails, the current
|
|
-- request will be interrupted and an error response will be returned.
|
|
--
|
|
-- NOTE: This is a higher wrapper for `check_token` function.
|
|
local function set_ctx_and_check_token()
|
|
local api_ctx = {}
|
|
core.ctx.set_vars_meta(api_ctx)
|
|
ngx.ctx.api_ctx = api_ctx
|
|
|
|
local ok, err = check_token(api_ctx)
|
|
if not ok then
|
|
core.log.warn("failed to check token: ", err)
|
|
core.response.exit(401, { error_msg = "failed to check token", description = err })
|
|
end
|
|
end
|
|
|
|
|
|
local function strip_etcd_resp(data)
|
|
if type(data) == "table"
|
|
and data.header ~= nil
|
|
and data.header.revision ~= nil
|
|
and data.header.raft_term ~= nil
|
|
then
|
|
-- strip etcd data
|
|
data.header = nil
|
|
data.responses = nil
|
|
data.succeeded = nil
|
|
|
|
if data.node then
|
|
data.node.createdIndex = nil
|
|
data.node.modifiedIndex = nil
|
|
end
|
|
|
|
data.count = nil
|
|
data.more = nil
|
|
data.prev_kvs = nil
|
|
|
|
if data.deleted then
|
|
-- We used to treat the type incorrectly. But for compatibility we follow
|
|
-- the existing type.
|
|
data.deleted = tostring(data.deleted)
|
|
end
|
|
end
|
|
|
|
return data
|
|
end
|
|
|
|
|
|
local function head()
|
|
core.response.exit(200)
|
|
end
|
|
|
|
|
|
local function run()
|
|
set_ctx_and_check_token()
|
|
|
|
local uri_segs = core.utils.split_uri(ngx.var.uri)
|
|
core.log.info("uri: ", core.json.delay_encode(uri_segs))
|
|
|
|
-- /apisix/admin/schema/route
|
|
local seg_res, seg_id = uri_segs[4], uri_segs[5]
|
|
local seg_sub_path = core.table.concat(uri_segs, "/", 6)
|
|
if seg_res == "schema" and seg_id == "plugins" then
|
|
-- /apisix/admin/schema/plugins/limit-count
|
|
seg_res, seg_id = uri_segs[5], uri_segs[6]
|
|
seg_sub_path = core.table.concat(uri_segs, "/", 7)
|
|
end
|
|
|
|
if seg_res == "stream_routes" then
|
|
local local_conf = core.config.local_conf()
|
|
if local_conf.apisix.proxy_mode ~= "stream" and
|
|
local_conf.apisix.proxy_mode ~= "http&stream" then
|
|
core.log.warn("stream mode is disabled, can not add any stream ",
|
|
"routes")
|
|
core.response.exit(400, {error_msg = "stream mode is disabled, " ..
|
|
"can not add stream routes"})
|
|
end
|
|
end
|
|
|
|
if seg_res == "consumers" and #uri_segs >= 6 and uri_segs[6] == "credentials" then
|
|
seg_sub_path = seg_id .. "/" .. seg_sub_path
|
|
seg_res = uri_segs[6]
|
|
seg_id = uri_segs[7]
|
|
end
|
|
|
|
local resource = resources[seg_res]
|
|
if not resource then
|
|
core.response.exit(404, {error_msg = "Unsupported resource type: ".. seg_res})
|
|
end
|
|
|
|
local method = str_lower(get_method())
|
|
if not resource[method] then
|
|
core.response.exit(404, {error_msg = "not found"})
|
|
end
|
|
|
|
local req_body, err = core.request.get_body(MAX_REQ_BODY)
|
|
if err then
|
|
core.log.error("failed to read request body: ", err)
|
|
core.response.exit(400, {error_msg = "invalid request body: " .. err})
|
|
end
|
|
|
|
if req_body then
|
|
local data, err = core.json.decode(req_body)
|
|
if err then
|
|
core.log.error("invalid request body: ", req_body, " err: ", err)
|
|
core.response.exit(400, {error_msg = "invalid request body: " .. err,
|
|
req_body = req_body})
|
|
end
|
|
|
|
req_body = data
|
|
end
|
|
|
|
local uri_args = ngx.req.get_uri_args() or {}
|
|
if uri_args.ttl then
|
|
if not tonumber(uri_args.ttl) then
|
|
core.response.exit(400, {error_msg = "invalid argument ttl: "
|
|
.. "should be a number"})
|
|
end
|
|
end
|
|
|
|
local code, data
|
|
if seg_res == "schema" or seg_res == "plugins" then
|
|
code, data = resource[method](seg_id, req_body, seg_sub_path, uri_args)
|
|
else
|
|
code, data = resource[method](resource, seg_id, req_body, seg_sub_path, uri_args)
|
|
end
|
|
|
|
if code then
|
|
if method == "get" and plugin.enable_data_encryption then
|
|
if seg_res == "consumers" or seg_res == "credentials" then
|
|
utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_CONSUMER)
|
|
elseif seg_res == "plugin_metadata" then
|
|
utils.decrypt_params(plugin.decrypt_conf, data, core.schema.TYPE_METADATA)
|
|
else
|
|
utils.decrypt_params(plugin.decrypt_conf, data)
|
|
end
|
|
end
|
|
|
|
if v3_adapter.enable_v3() then
|
|
core.response.set_header("X-API-VERSION", "v3")
|
|
else
|
|
core.response.set_header("X-API-VERSION", "v2")
|
|
end
|
|
|
|
data = v3_adapter.filter(data, resource)
|
|
data = strip_etcd_resp(data)
|
|
|
|
core.response.exit(code, data)
|
|
end
|
|
end
|
|
|
|
|
|
local function get_plugins_list()
|
|
set_ctx_and_check_token()
|
|
local args = get_uri_args()
|
|
local subsystem = args["subsystem"]
|
|
-- If subsystem is passed then it should be either http or stream.
|
|
-- If it is not passed/nil then http will be default.
|
|
subsystem = subsystem or "http"
|
|
if subsystem == "http" or subsystem == "stream" then
|
|
local plugins = resources.plugins.get_plugins_list(subsystem)
|
|
core.response.exit(200, plugins)
|
|
end
|
|
core.response.exit(400,"invalid subsystem passed")
|
|
end
|
|
|
|
-- Handle unsupported request methods for the virtual "reload" plugin
|
|
local function unsupported_methods_reload_plugin()
|
|
set_ctx_and_check_token()
|
|
|
|
core.response.exit(405, {
|
|
error_msg = "please use PUT method to reload the plugins, "
|
|
.. get_method() .. " method is not allowed."
|
|
})
|
|
end
|
|
|
|
|
|
local function post_reload_plugins()
|
|
set_ctx_and_check_token()
|
|
|
|
local success, err = events:post(reload_event, get_method(), ngx_time())
|
|
if not success then
|
|
core.response.exit(503, err)
|
|
end
|
|
|
|
core.response.exit(200, "done")
|
|
end
|
|
|
|
|
|
local function plugins_eq(old, new)
|
|
local old_set = {}
|
|
for _, p in ipairs(old) do
|
|
old_set[p.name] = p
|
|
end
|
|
|
|
local new_set = {}
|
|
for _, p in ipairs(new) do
|
|
new_set[p.name] = p
|
|
end
|
|
|
|
return core.table.set_eq(old_set, new_set)
|
|
end
|
|
|
|
|
|
local function sync_local_conf_to_etcd(reset)
|
|
local local_conf = core.config.local_conf()
|
|
|
|
local plugins = {}
|
|
for _, name in ipairs(local_conf.plugins) do
|
|
core.table.insert(plugins, {
|
|
name = name,
|
|
})
|
|
end
|
|
|
|
for _, name in ipairs(local_conf.stream_plugins) do
|
|
core.table.insert(plugins, {
|
|
name = name,
|
|
stream = true,
|
|
})
|
|
end
|
|
|
|
if reset then
|
|
local res, err = core.etcd.get("/plugins")
|
|
if not res then
|
|
core.log.error("failed to get current plugins: ", err)
|
|
return
|
|
end
|
|
|
|
if res.status == 404 then
|
|
-- nothing need to be reset
|
|
return
|
|
end
|
|
|
|
if res.status ~= 200 then
|
|
core.log.error("failed to get current plugins, status: ", res.status)
|
|
return
|
|
end
|
|
|
|
local stored_plugins = res.body.node.value
|
|
local revision = res.body.node.modifiedIndex
|
|
if plugins_eq(stored_plugins, plugins) then
|
|
core.log.info("plugins not changed, don't need to reset")
|
|
return
|
|
end
|
|
|
|
core.log.warn("sync local conf to etcd")
|
|
|
|
local res, err = core.etcd.atomic_set("/plugins", plugins, nil, revision)
|
|
if not res then
|
|
core.log.error("failed to set plugins: ", err)
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
core.log.warn("sync local conf to etcd")
|
|
|
|
-- need to store all plugins name into one key so that it can be updated atomically
|
|
local res, err = core.etcd.set("/plugins", plugins)
|
|
if not res then
|
|
core.log.error("failed to set plugins: ", err)
|
|
end
|
|
end
|
|
|
|
|
|
local function reload_plugins(data, event, source, pid)
|
|
core.log.info("start to hot reload plugins")
|
|
plugin.load()
|
|
|
|
if ngx_worker_id() == 0 then
|
|
sync_local_conf_to_etcd()
|
|
end
|
|
end
|
|
|
|
|
|
local function schema_validate()
|
|
local uri_segs = core.utils.split_uri(ngx.var.uri)
|
|
core.log.info("uri: ", core.json.delay_encode(uri_segs))
|
|
|
|
local seg_res = uri_segs[6]
|
|
local resource = resources[seg_res]
|
|
if not resource then
|
|
core.response.exit(404, {error_msg = "Unsupported resource type: ".. seg_res})
|
|
end
|
|
|
|
local req_body, err = core.request.get_body(MAX_REQ_BODY)
|
|
if err then
|
|
core.log.error("failed to read request body: ", err)
|
|
core.response.exit(400, {error_msg = "invalid request body: " .. err})
|
|
end
|
|
|
|
if req_body then
|
|
local data, err = core.json.decode(req_body)
|
|
if err then
|
|
core.log.error("invalid request body: ", req_body, " err: ", err)
|
|
core.response.exit(400, {error_msg = "invalid request body: " .. err,
|
|
req_body = req_body})
|
|
end
|
|
|
|
req_body = data
|
|
end
|
|
|
|
local ok, err = core.schema.check(resource.schema, req_body)
|
|
if ok then
|
|
core.response.exit(200)
|
|
end
|
|
core.response.exit(400, {error_msg = err})
|
|
end
|
|
|
|
|
|
local function standalone_run()
|
|
set_ctx_and_check_token()
|
|
return standalone.run()
|
|
end
|
|
|
|
|
|
local http_head_route = {
|
|
paths = [[/apisix/admin]],
|
|
methods = {"HEAD"},
|
|
handler = head,
|
|
}
|
|
|
|
|
|
local uri_route = {
|
|
http_head_route,
|
|
{
|
|
paths = [[/apisix/admin/*]],
|
|
methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
|
|
handler = run,
|
|
},
|
|
{
|
|
paths = [[/apisix/admin/plugins/list]],
|
|
methods = {"GET"},
|
|
handler = get_plugins_list,
|
|
},
|
|
{
|
|
paths = [[/apisix/admin/schema/validate/*]],
|
|
methods = {"POST"},
|
|
handler = schema_validate,
|
|
},
|
|
{
|
|
paths = reload_event,
|
|
methods = {"PUT"},
|
|
handler = post_reload_plugins,
|
|
},
|
|
-- Handle methods other than "PUT" on "/plugin/reload" to inform user
|
|
{
|
|
paths = reload_event,
|
|
methods = { "GET", "POST", "DELETE", "PATCH" },
|
|
handler = unsupported_methods_reload_plugin,
|
|
},
|
|
}
|
|
|
|
|
|
local standalone_uri_route = {
|
|
http_head_route,
|
|
{
|
|
paths = [[/apisix/admin/configs]],
|
|
methods = {"GET", "PUT"},
|
|
handler = standalone_run,
|
|
},
|
|
}
|
|
|
|
|
|
function _M.init_worker()
|
|
local local_conf = core.config.local_conf()
|
|
if not local_conf.apisix or not local_conf.apisix.enable_admin then
|
|
return
|
|
end
|
|
|
|
local is_yaml_config_provider = local_conf.deployment.config_provider == "yaml"
|
|
|
|
if is_yaml_config_provider then
|
|
router = route.new(standalone_uri_route)
|
|
standalone.init_worker()
|
|
else
|
|
router = route.new(uri_route)
|
|
end
|
|
|
|
-- register reload plugin handler
|
|
events = require("apisix.events")
|
|
events:register(reload_plugins, reload_event, "PUT")
|
|
|
|
if ngx_worker_id() == 0 then
|
|
-- check if admin_key is required
|
|
if local_conf.deployment.admin.admin_key_required == false then
|
|
core.log.warn("Admin key is bypassed! ",
|
|
"If you are deploying APISIX in a production environment, ",
|
|
"please enable `admin_key_required` and set a secure admin key!")
|
|
end
|
|
|
|
if is_yaml_config_provider then -- standalone mode does not need sync to etcd
|
|
return
|
|
end
|
|
|
|
local ok, err = ngx_timer_at(0, function(premature)
|
|
if premature then
|
|
return
|
|
end
|
|
|
|
-- try to reset the /plugins to the current configuration in the admin
|
|
sync_local_conf_to_etcd(true)
|
|
end)
|
|
|
|
if not ok then
|
|
error("failed to sync local configure to etcd: " .. err)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function _M.get()
|
|
return router
|
|
end
|
|
|
|
|
|
return _M
|