blob: 74858c7ab446c7876b5730498ffdc3791291da09 [file] [log] [blame]
--
-- 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 route = require("apisix.utils.router")
local plugin = require("apisix.plugin")
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 str_lower = string.lower
local reload_event = "/apisix/admin/plugins/reload"
local ipairs = ipairs
local error = error
local type = type
local req_read_body = ngx.req.read_body
local req_get_body_data = ngx.req.get_body_data
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"),
schema = require("apisix.admin.schema"),
ssl = require("apisix.admin.ssl"),
plugins = require("apisix.admin.plugins"),
proto = 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"),
}
local _M = {version = 0.4}
local router
local function check_token(ctx)
local local_conf = core.config.local_conf()
if not local_conf or not local_conf.apisix
or not local_conf.apisix.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(local_conf.apisix.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
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
end
return data
end
local function run()
local api_ctx = {}
core.ctx.set_vars_meta(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)
end
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
local resource = resources[seg_res]
if not resource then
core.response.exit(404)
end
local method = str_lower(get_method())
if not resource[method] then
core.response.exit(404)
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 not data then
core.log.error("invalid request body: ", req_body, " err: ", err)
core.response.exit(400, {error_msg = "invalid request body",
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 = resource[method](seg_id, req_body, seg_sub_path,
uri_args)
if code then
data = strip_etcd_resp(data)
core.response.exit(code, data)
end
end
local function run_stream()
local api_ctx = {}
core.ctx.set_vars_meta(api_ctx)
local local_conf = core.config.local_conf()
if not local_conf.apisix.stream_proxy then
core.log.warn("stream mode is disabled, can not to add any stream ",
"route")
core.response.exit(400)
end
local ok, err = check_token(api_ctx)
if not ok then
core.log.warn("failed to check token: ", err)
core.response.exit(401)
end
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
local resource = resources[seg_res]
if not resource then
core.response.exit(404)
end
local method = str_lower(get_method())
if not resource[method] then
core.response.exit(404)
end
req_read_body()
local req_body = req_get_body_data()
if req_body then
local data, err = core.json.decode(req_body)
if not data then
core.log.error("invalid request body: ", req_body, " err: ", err)
core.response.exit(400, {error_msg = "invalid request body",
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 = resource[method](seg_id, req_body, seg_sub_path,
uri_args)
if code then
data = strip_etcd_resp(data)
core.response.exit(code, data)
end
end
local function get_plugins_list()
local api_ctx = {}
core.ctx.set_vars_meta(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)
end
local plugins = resources.plugins.get_plugins_list()
core.response.exit(200, plugins)
end
local function post_reload_plugins()
local api_ctx = {}
core.ctx.set_vars_meta(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)
end
local success, err = events.post(reload_event, get_method(), ngx_time())
if not success then
core.response.exit(500, err)
end
core.response.exit(200, "done")
end
local function sync_local_conf_to_etcd()
core.log.warn("sync local conf to etcd")
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
-- 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 uri_route = {
{
paths = [[/apisix/admin/*]],
methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
handler = run,
},
{
paths = [[/apisix/admin/stream_routes/*]],
methods = {"GET", "PUT", "POST", "DELETE", "PATCH"},
handler = run_stream,
},
{
paths = [[/apisix/admin/plugins/list]],
methods = {"GET"},
handler = get_plugins_list,
},
{
paths = reload_event,
methods = {"PUT"},
handler = post_reload_plugins,
},
}
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
router = route.new(uri_route)
events = require("resty.worker.events")
events.register(reload_plugins, reload_event, "PUT")
if ngx_worker_id() == 0 then
local ok, err = ngx_timer_at(0, function(premature)
if premature then
return
end
sync_local_conf_to_etcd()
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