blob: fbf01608ae87cd88e86e131381da2208e065a7c3 [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 ver = require("apisix.core.version")
local etcd = require("apisix.cli.etcd")
local util = require("apisix.cli.util")
local file = require("apisix.cli.file")
local ngx_tpl = require("apisix.cli.ngx_tpl")
local profile = require("apisix.core.profile")
local template = require("resty.template")
local argparse = require("argparse")
local pl_path = require("pl.path")
local jsonschema = require("jsonschema")
local stderr = io.stderr
local ipairs = ipairs
local pairs = pairs
local print = print
local type = type
local tostring = tostring
local tonumber = tonumber
local io_open = io.open
local popen = io.popen
local execute = os.execute
local table_insert = table.insert
local getenv = os.getenv
local max = math.max
local floor = math.floor
local str_find = string.find
local str_byte = string.byte
local str_sub = string.sub
local _M = {}
local function help()
print([[
Usage: apisix [action] <argument>
help: show this message, then exit
init: initialize the local nginx.conf
init_etcd: initialize the data of etcd
start: start the apisix server
stop: stop the apisix server
restart: restart the apisix server
reload: reload the apisix server
version: print the version of apisix
]])
end
local function check_version(cur_ver_s, need_ver_s)
local cur_vers = util.split(cur_ver_s, [[.]])
local need_vers = util.split(need_ver_s, [[.]])
local len = max(#cur_vers, #need_vers)
for i = 1, len do
local cur_ver = tonumber(cur_vers[i]) or 0
local need_ver = tonumber(need_vers[i]) or 0
if cur_ver > need_ver then
return true
end
if cur_ver < need_ver then
return false
end
end
return true
end
local function get_openresty_version()
local str = "nginx version: openresty/"
local ret = util.execute_cmd("openresty -v 2>&1")
local pos = str_find(ret, str, 1, true)
if pos then
return str_sub(ret, pos + #str)
end
str = "nginx version: nginx/"
pos = str_find(ret, str, 1, true)
if pos then
return str_sub(ret, pos + #str)
end
end
local function local_dns_resolver(file_path)
local file, err = io_open(file_path, "rb")
if not file then
return false, "failed to open file: " .. file_path .. ", error info:" .. err
end
local dns_addrs = {}
for line in file:lines() do
local addr, n = line:gsub("^nameserver%s+([^%s]+)%s*$", "%1")
if n == 1 then
table_insert(dns_addrs, addr)
end
end
file:close()
return dns_addrs
end
-- exported for test
_M.local_dns_resolver = local_dns_resolver
local function version()
print(ver['VERSION'])
end
local function get_lua_path(conf)
-- we use "" as the placeholder to enforce the type to be string
if conf and conf ~= "" then
if #conf < 2 then
-- the shortest valid path is ';;'
util.die("invalid extra_lua_path/extra_lua_cpath: \"", conf, "\"\n")
end
local path = conf
if path:byte(-1) ~= str_byte(';') then
path = path .. ';'
end
return path
end
return ""
end
local config_schema = {
type = "object",
properties = {
apisix = {
properties = {
config_center = {
enum = {"etcd", "yaml"},
},
proxy_protocol = {
type = "object",
properties = {
listen_http_port = {
type = "integer",
},
listen_https_port = {
type = "integer",
},
enable_tcp_pp = {
type = "boolean",
},
enable_tcp_pp_to_upstream = {
type = "boolean",
},
}
},
port_admin = {
type = "integer",
},
https_admin = {
type = "boolean",
},
stream_proxy = {
type = "object",
properties = {
tcp = {
type = "array",
minItems = 1,
items = {
anyOf = {
{
type = "integer",
},
{
type = "string",
},
},
},
uniqueItems = true,
},
udp = {
type = "array",
minItems = 1,
items = {
anyOf = {
{
type = "integer",
},
{
type = "string",
},
},
},
uniqueItems = true,
},
}
},
dns_resolver = {
type = "array",
minItems = 1,
items = {
type = "string",
}
},
dns_resolver_valid = {
type = "integer",
},
ssl = {
type = "object",
properties = {
ssl_trusted_certificate = {
type = "string",
}
}
},
}
},
nginx_config = {
type = "object",
properties = {
envs = {
type = "array",
minItems = 1,
items = {
type = "string",
}
}
},
},
http = {
type = "object",
properties = {
lua_shared_dicts = {
type = "object",
}
}
},
etcd = {
type = "object",
properties = {
resync_delay = {
type = "integer",
},
user = {
type = "string",
},
password = {
type = "string",
},
tls = {
type = "object",
properties = {
cert = {
type = "string",
},
key = {
type = "string",
},
}
}
}
}
}
}
local function init(env)
if env.is_root_path then
print('Warning! Running apisix under /root is only suitable for '
.. 'development environments and it is dangerous to do so. '
.. 'It is recommended to run APISIX in a directory '
.. 'other than /root.')
end
-- read_yaml_conf
local yaml_conf, err = file.read_yaml_conf(env.apisix_home)
if not yaml_conf then
util.die("failed to read local yaml config of apisix: ", err, "\n")
end
local validator = jsonschema.generate_validator(config_schema)
local ok, err = validator(yaml_conf)
if not ok then
util.die("failed to validate config: ", err, "\n")
end
-- check the Admin API token
local checked_admin_key = false
if yaml_conf.apisix.enable_admin and yaml_conf.apisix.allow_admin then
for _, allow_ip in ipairs(yaml_conf.apisix.allow_admin) do
if allow_ip == "127.0.0.0/24" then
checked_admin_key = true
end
end
end
if yaml_conf.apisix.enable_admin and not checked_admin_key then
local help = [[
%s
Please modify "admin_key" in conf/config.yaml .
]]
if type(yaml_conf.apisix.admin_key) ~= "table" or
#yaml_conf.apisix.admin_key == 0
then
util.die(help:format("ERROR: missing valid Admin API token."))
end
for _, admin in ipairs(yaml_conf.apisix.admin_key) do
if type(admin.key) == "table" then
admin.key = ""
else
admin.key = tostring(admin.key)
end
if admin.key == "" then
util.die(help:format("ERROR: missing valid Admin API token."), "\n")
end
if admin.key == "edd1c9f034335f136f87ad84b625c8f1" then
stderr:write(
help:format([[WARNING: using fixed Admin API token has security risk.]]),
"\n"
)
end
end
end
if yaml_conf.apisix.enable_admin and
yaml_conf.apisix.config_center == "yaml"
then
util.die("ERROR: Admin API can only be used with etcd config_center.\n")
end
local or_ver = get_openresty_version()
if or_ver == nil then
util.die("can not find openresty\n")
end
local need_ver = "1.17.3"
if not check_version(or_ver, need_ver) then
util.die("openresty version must >=", need_ver, " current ", or_ver, "\n")
end
local or_info = util.execute_cmd("openresty -V 2>&1")
local with_module_status = true
if or_info and not or_info:find("http_stub_status_module", 1, true) then
stderr:write("'http_stub_status_module' module is missing in ",
"your openresty, please check it out. Without this ",
"module, there will be fewer monitoring indicators.\n")
with_module_status = false
end
local enabled_plugins = {}
for i, name in ipairs(yaml_conf.plugins) do
enabled_plugins[name] = true
end
if enabled_plugins["proxy-cache"] and not yaml_conf.apisix.proxy_cache then
util.die("missing apisix.proxy_cache for plugin proxy-cache\n")
end
-- support multiple ports listen, compatible with the original style
if type(yaml_conf.apisix.node_listen) == "number" then
local node_listen = {{port = yaml_conf.apisix.node_listen}}
yaml_conf.apisix.node_listen = node_listen
elseif type(yaml_conf.apisix.node_listen) == "table" then
local node_listen = {}
for index, value in ipairs(yaml_conf.apisix.node_listen) do
if type(value) == "number" then
table_insert(node_listen, index, {port = value})
elseif type(value) == "table" then
table_insert(node_listen, index, value)
end
end
yaml_conf.apisix.node_listen = node_listen
end
if type(yaml_conf.apisix.ssl.listen_port) == "number" then
local listen_port = {yaml_conf.apisix.ssl.listen_port}
yaml_conf.apisix.ssl.listen_port = listen_port
end
if yaml_conf.apisix.ssl.ssl_trusted_certificate ~= nil then
local cert_path = yaml_conf.apisix.ssl.ssl_trusted_certificate
-- During validation, the path is relative to PWD
-- When Nginx starts, the path is relative to conf
-- Therefore we need to check the absolute version instead
cert_path = pl_path.abspath(cert_path)
local ok, err = util.is_file_exist(cert_path)
if not ok then
util.die(err, "\n")
end
yaml_conf.apisix.ssl.ssl_trusted_certificate = cert_path
end
local admin_api_mtls = yaml_conf.apisix.admin_api_mtls
if yaml_conf.apisix.https_admin and
not (admin_api_mtls and
admin_api_mtls.admin_ssl_cert and
admin_api_mtls.admin_ssl_cert ~= "" and
admin_api_mtls.admin_ssl_cert_key and
admin_api_mtls.admin_ssl_cert_key ~= "")
then
util.die("missing ssl cert for https admin")
end
-- enable ssl with place holder crt&key
yaml_conf.apisix.ssl.ssl_cert = "cert/ssl_PLACE_HOLDER.crt"
yaml_conf.apisix.ssl.ssl_cert_key = "cert/ssl_PLACE_HOLDER.key"
local dubbo_upstream_multiplex_count = 32
if yaml_conf.plugin_attr and yaml_conf.plugin_attr["dubbo-proxy"] then
local dubbo_conf = yaml_conf.plugin_attr["dubbo-proxy"]
if tonumber(dubbo_conf.upstream_multiplex_count) >= 1 then
dubbo_upstream_multiplex_count = dubbo_conf.upstream_multiplex_count
end
end
if yaml_conf.apisix.dns_resolver_valid then
if tonumber(yaml_conf.apisix.dns_resolver_valid) == nil then
util.die("apisix->dns_resolver_valid should be a number")
end
end
-- Using template.render
local sys_conf = {
lua_path = env.pkg_path_org,
lua_cpath = env.pkg_cpath_org,
os_name = util.trim(util.execute_cmd("uname")),
apisix_lua_home = env.apisix_home,
with_module_status = with_module_status,
error_log = {level = "warn"},
enabled_plugins = enabled_plugins,
dubbo_upstream_multiplex_count = dubbo_upstream_multiplex_count,
}
if not yaml_conf.apisix then
util.die("failed to read `apisix` field from yaml file")
end
if not yaml_conf.nginx_config then
util.die("failed to read `nginx_config` field from yaml file")
end
if util.is_32bit_arch() then
sys_conf["worker_rlimit_core"] = "4G"
else
sys_conf["worker_rlimit_core"] = "16G"
end
for k,v in pairs(yaml_conf.apisix) do
sys_conf[k] = v
end
for k,v in pairs(yaml_conf.nginx_config) do
sys_conf[k] = v
end
if yaml_conf.apisix.enable_control then
if not yaml_conf.apisix.control then
sys_conf.control_server_addr = "127.0.0.1:9090"
else
local ip = yaml_conf.apisix.control.ip
local port = tonumber(yaml_conf.apisix.control.port)
if ip == nil then
ip = "127.0.0.1"
end
if not port then
port = 9090
end
sys_conf.control_server_addr = ip .. ":" .. port
end
end
if yaml_conf.plugin_attr.prometheus then
local prometheus = yaml_conf.plugin_attr.prometheus
if prometheus.enable_export_server then
local ip = prometheus.export_addr.ip
local port = tonumber(prometheus.export_addr.port)
if ip == nil then
ip = "127.0.0.1"
end
if not port then
port = 9091
end
sys_conf.prometheus_server_addr = ip .. ":" .. port
end
end
local wrn = sys_conf["worker_rlimit_nofile"]
local wc = sys_conf["event"]["worker_connections"]
if not wrn or wrn <= wc then
-- ensure the number of fds is slightly larger than the number of conn
sys_conf["worker_rlimit_nofile"] = wc + 128
end
if sys_conf["enable_dev_mode"] == true then
sys_conf["worker_processes"] = 1
sys_conf["enable_reuseport"] = false
elseif tonumber(sys_conf["worker_processes"]) == nil then
sys_conf["worker_processes"] = "auto"
end
if sys_conf.allow_admin and #sys_conf.allow_admin == 0 then
sys_conf.allow_admin = nil
end
local dns_resolver = sys_conf["dns_resolver"]
if not dns_resolver or #dns_resolver == 0 then
local dns_addrs, err = local_dns_resolver("/etc/resolv.conf")
if not dns_addrs then
util.die("failed to import local DNS: ", err, "\n")
end
if #dns_addrs == 0 then
util.die("local DNS is empty\n")
end
sys_conf["dns_resolver"] = dns_addrs
end
local env_worker_processes = getenv("APISIX_WORKER_PROCESSES")
if env_worker_processes then
sys_conf["worker_processes"] = floor(tonumber(env_worker_processes))
end
local exported_vars = file.get_exported_vars()
if exported_vars then
if not sys_conf["envs"] then
sys_conf["envs"]= {}
end
for _, cfg_env in ipairs(sys_conf["envs"]) do
local cfg_name
local from = str_find(cfg_env, "=", 1, true)
if from then
cfg_name = str_sub(cfg_env, 1, from - 1)
else
cfg_name = cfg_env
end
exported_vars[cfg_name] = false
end
for name, value in pairs(exported_vars) do
if value then
table_insert(sys_conf["envs"], name .. "=" .. value)
end
end
end
-- fix up lua path
sys_conf["extra_lua_path"] = get_lua_path(yaml_conf.apisix.extra_lua_path)
sys_conf["extra_lua_cpath"] = get_lua_path(yaml_conf.apisix.extra_lua_cpath)
local conf_render = template.compile(ngx_tpl)
local ngxconf = conf_render(sys_conf)
local ok, err = util.write_file(env.apisix_home .. "/conf/nginx.conf",
ngxconf)
if not ok then
util.die("failed to update nginx.conf: ", err, "\n")
end
end
local function init_etcd(env, args)
etcd.init(env, args)
end
local function start(env, ...)
-- Because the worker process started by apisix has "nobody" permission,
-- it cannot access the `/root` directory. Therefore, it is necessary to
-- prohibit APISIX from running in the /root directory.
if env.is_root_path then
util.die("Error: It is forbidden to run APISIX in the /root directory.\n")
end
local cmd_logs = "mkdir -p " .. env.apisix_home .. "/logs"
util.execute_cmd(cmd_logs)
-- check running
local pid_path = env.apisix_home .. "/logs/nginx.pid"
local pid = util.read_file(pid_path)
pid = tonumber(pid)
if pid then
local lsof_cmd = "lsof -p " .. pid
local hd = popen(lsof_cmd)
local res = hd:read("*a")
if not (res and res == "") then
if not res then
print("failed to read the result of command: " .. lsof_cmd)
else
print("APISIX is running...")
end
return
end
print("nginx.pid exists but there's no corresponding process with pid ", pid,
", the file will be overwritten")
end
local parser = argparse()
parser:argument("_", "Placeholder")
parser:option("-c --config", "location of customized config.yaml")
-- TODO: more logs for APISIX cli could be added using this feature
parser:flag("--verbose", "show init_etcd debug information")
local args = parser:parse()
local customized_yaml = args["config"]
if customized_yaml then
profile.apisix_home = env.apisix_home .. "/"
local local_conf_path = profile:yaml_path("config")
util.execute_cmd("mv " .. local_conf_path .. " " .. local_conf_path .. ".bak")
util.execute_cmd("ln " .. customized_yaml .. " " .. local_conf_path)
print("Use customized yaml: ", customized_yaml)
end
init(env)
init_etcd(env, args)
util.execute_cmd(env.openresty_args)
end
local function stop(env)
local local_conf_path = profile:yaml_path("config")
local bak_exist = io_open(local_conf_path .. ".bak")
if bak_exist then
util.execute_cmd("rm " .. local_conf_path)
util.execute_cmd("mv " .. local_conf_path .. ".bak " .. local_conf_path)
end
local cmd = env.openresty_args .. [[ -s stop]]
util.execute_cmd(cmd)
end
local function restart(env)
stop(env)
start(env)
end
local function reload(env)
-- reinit nginx.conf
init(env)
local test_cmd = env.openresty_args .. [[ -t -q ]]
-- When success,
-- On linux, os.execute returns 0,
-- On macos, os.execute returns 3 values: true, exit, 0, and we need the first.
local test_ret = execute((test_cmd))
if (test_ret == 0 or test_ret == true) then
local cmd = env.openresty_args .. [[ -s reload]]
execute(cmd)
return
end
print("test openresty failed")
end
local action = {
help = help,
version = version,
init = init,
init_etcd = etcd.init,
start = start,
stop = stop,
restart = restart,
reload = reload,
}
function _M.execute(env, arg)
local cmd_action = arg[1]
if not cmd_action then
return help()
end
if not action[cmd_action] then
stderr:write("invalid argument: ", cmd_action, "\n")
return help()
end
action[cmd_action](env, arg[2])
end
return _M