- 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>
344 lines
10 KiB
Lua
344 lines
10 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 ngx = ngx
|
|
local yaml = require("lyaml")
|
|
local profile = require("apisix.core.profile")
|
|
local util = require("apisix.cli.util")
|
|
local schema = require("apisix.cli.schema")
|
|
local default_conf = require("apisix.cli.config")
|
|
local dkjson = require("dkjson")
|
|
local pl_path = require("pl.path")
|
|
|
|
local pairs = pairs
|
|
local type = type
|
|
local tonumber = tonumber
|
|
local getenv = os.getenv
|
|
local str_gmatch = string.gmatch
|
|
local str_find = string.find
|
|
local str_sub = string.sub
|
|
local print = print
|
|
|
|
local _M = {}
|
|
local exported_vars
|
|
|
|
|
|
function _M.get_exported_vars()
|
|
return exported_vars
|
|
end
|
|
|
|
|
|
local function is_empty_yaml_line(line)
|
|
return line == '' or str_find(line, '^%s*$') or str_find(line, '^%s*#')
|
|
end
|
|
|
|
|
|
local function tab_is_array(t)
|
|
local count = 0
|
|
for k, v in pairs(t) do
|
|
count = count + 1
|
|
end
|
|
|
|
return #t == count
|
|
end
|
|
|
|
|
|
local function var_sub(val)
|
|
local err
|
|
local var_used = false
|
|
-- we use '${{var}}' because '$var' and '${var}' are taken
|
|
-- by Nginx
|
|
local new_val = val:gsub("%$%{%{%s*([%w_]+[%:%=]?.-)%s*%}%}", function(var)
|
|
local i, j = var:find("%:%=")
|
|
local default
|
|
if i and j then
|
|
default = var:sub(i + 2, #var)
|
|
default = default:gsub('^%s*(.-)%s*$', '%1')
|
|
var = var:sub(1, i - 1)
|
|
end
|
|
|
|
local v = getenv(var) or default
|
|
if v then
|
|
if not exported_vars then
|
|
exported_vars = {}
|
|
end
|
|
|
|
exported_vars[var] = v
|
|
var_used = true
|
|
return v
|
|
end
|
|
|
|
err = "failed to handle configuration: " ..
|
|
"can't find environment variable " .. var
|
|
return ""
|
|
end)
|
|
return new_val, var_used, err
|
|
end
|
|
|
|
|
|
local function resolve_conf_var(conf)
|
|
local new_keys = {}
|
|
for key, val in pairs(conf) do
|
|
-- avoid re-iterating the table for already iterated key
|
|
if new_keys[key] then
|
|
goto continue
|
|
end
|
|
-- substitute environment variables from conf keys
|
|
if type(key) == "string" then
|
|
local new_key, _, err = var_sub(key)
|
|
if err then
|
|
return nil, err
|
|
end
|
|
if new_key ~= key then
|
|
new_keys[new_key] = "dummy" -- we only care about checking the key
|
|
conf.key = nil
|
|
conf[new_key] = val
|
|
key = new_key
|
|
end
|
|
end
|
|
if type(val) == "table" then
|
|
local ok, err = resolve_conf_var(val)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
elseif type(val) == "string" then
|
|
local new_val, var_used, err = var_sub(val)
|
|
|
|
if err then
|
|
return nil, err
|
|
end
|
|
|
|
if var_used then
|
|
if tonumber(new_val) ~= nil then
|
|
new_val = tonumber(new_val)
|
|
elseif new_val == "true" then
|
|
new_val = true
|
|
elseif new_val == "false" then
|
|
new_val = false
|
|
end
|
|
end
|
|
|
|
conf[key] = new_val
|
|
end
|
|
::continue::
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
_M.resolve_conf_var = resolve_conf_var
|
|
|
|
|
|
local function replace_by_reserved_env_vars(conf)
|
|
-- TODO: support more reserved environment variables
|
|
local v = getenv("APISIX_DEPLOYMENT_ETCD_HOST")
|
|
if v and conf["deployment"] and conf["deployment"]["etcd"] then
|
|
local val, _, err = dkjson.decode(v)
|
|
if err or not val then
|
|
print("parse ${APISIX_DEPLOYMENT_ETCD_HOST} failed, error:", err)
|
|
return
|
|
end
|
|
|
|
conf["deployment"]["etcd"]["host"] = val
|
|
end
|
|
end
|
|
|
|
|
|
local function path_is_multi_type(path, type_val)
|
|
if str_sub(path, 1, 14) == "nginx_config->" and
|
|
(type_val == "number" or type_val == "string") then
|
|
return true
|
|
end
|
|
|
|
if path == "apisix->node_listen" and type_val == "number" then
|
|
return true
|
|
end
|
|
|
|
if path == "apisix->data_encryption->keyring" then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
local function merge_conf(base, new_tab, ppath)
|
|
ppath = ppath or ""
|
|
|
|
for key, val in pairs(new_tab) do
|
|
if type(val) == "table" then
|
|
if val == yaml.null then
|
|
base[key] = nil
|
|
|
|
elseif tab_is_array(val) then
|
|
base[key] = val
|
|
|
|
else
|
|
if base[key] == nil then
|
|
base[key] = {}
|
|
end
|
|
|
|
local ok, err = merge_conf(
|
|
base[key],
|
|
val,
|
|
ppath == "" and key or ppath .. "->" .. key
|
|
)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
end
|
|
else
|
|
local type_val = type(val)
|
|
|
|
if base[key] == nil then
|
|
base[key] = val
|
|
elseif type(base[key]) ~= type_val then
|
|
local path = ppath == "" and key or ppath .. "->" .. key
|
|
|
|
if path_is_multi_type(path, type_val) then
|
|
base[key] = val
|
|
else
|
|
return nil, "failed to merge, path[" .. path .. "] expect: " ..
|
|
type(base[key]) .. ", but got: " .. type_val
|
|
end
|
|
else
|
|
base[key] = val
|
|
end
|
|
end
|
|
end
|
|
|
|
return base
|
|
end
|
|
|
|
|
|
function _M.read_yaml_conf(apisix_home)
|
|
if apisix_home then
|
|
profile.apisix_home = apisix_home .. "/"
|
|
end
|
|
|
|
local local_conf_path = profile:customized_yaml_path()
|
|
if not local_conf_path then
|
|
local_conf_path = profile:yaml_path("config")
|
|
end
|
|
local user_conf_yaml, err = util.read_file(local_conf_path)
|
|
if not user_conf_yaml then
|
|
return nil, err
|
|
end
|
|
|
|
local is_empty_file = true
|
|
for line in str_gmatch(user_conf_yaml .. '\n', '(.-)\r?\n') do
|
|
if not is_empty_yaml_line(line) then
|
|
is_empty_file = false
|
|
break
|
|
end
|
|
end
|
|
|
|
if not is_empty_file then
|
|
local user_conf = yaml.load(user_conf_yaml)
|
|
if not user_conf then
|
|
return nil, "invalid config.yaml file"
|
|
end
|
|
|
|
local ok, err = resolve_conf_var(user_conf)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
|
|
ok, err = merge_conf(default_conf, user_conf)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
end
|
|
|
|
-- fill the default value by the schema
|
|
local ok, err = schema.validate(default_conf)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
if default_conf.deployment then
|
|
default_conf.deployment.config_provider = "etcd"
|
|
if default_conf.deployment.role == "traditional" then
|
|
default_conf.etcd = default_conf.deployment.etcd
|
|
if default_conf.deployment.role_traditional.config_provider == "yaml" then
|
|
default_conf.deployment.config_provider = "yaml"
|
|
end
|
|
|
|
elseif default_conf.deployment.role == "control_plane" then
|
|
default_conf.etcd = default_conf.deployment.etcd
|
|
default_conf.apisix.enable_admin = true
|
|
|
|
elseif default_conf.deployment.role == "data_plane" then
|
|
default_conf.etcd = default_conf.deployment.etcd
|
|
if default_conf.deployment.role_data_plane.config_provider == "yaml" then
|
|
default_conf.deployment.config_provider = "yaml"
|
|
elseif default_conf.deployment.role_data_plane.config_provider == "json" then
|
|
default_conf.deployment.config_provider = "json"
|
|
elseif default_conf.deployment.role_data_plane.config_provider == "xds" then
|
|
default_conf.deployment.config_provider = "xds"
|
|
end
|
|
default_conf.apisix.enable_admin = false
|
|
end
|
|
end
|
|
|
|
--- using `not ngx` to check whether the current execution environment is apisix cli module,
|
|
--- because it is only necessary to parse and validate `apisix.yaml` in apisix cli.
|
|
if default_conf.deployment.config_provider == "yaml" and not ngx then
|
|
local apisix_conf_path = profile:yaml_path("apisix")
|
|
local apisix_conf_yaml, _ = util.read_file(apisix_conf_path)
|
|
if apisix_conf_yaml then
|
|
local apisix_conf = yaml.load(apisix_conf_yaml)
|
|
if apisix_conf then
|
|
local ok, err = resolve_conf_var(apisix_conf)
|
|
if not ok then
|
|
return nil, err
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local apisix_ssl = default_conf.apisix.ssl
|
|
if apisix_ssl and apisix_ssl.ssl_trusted_certificate then
|
|
-- default value is set to "system" during schema validation
|
|
if apisix_ssl.ssl_trusted_certificate == "system" then
|
|
local trusted_certs_path, err = util.get_system_trusted_certs_filepath()
|
|
if not trusted_certs_path then
|
|
util.die(err)
|
|
end
|
|
|
|
apisix_ssl.ssl_trusted_certificate = trusted_certs_path
|
|
else
|
|
-- 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
|
|
local cert_path = pl_path.abspath(apisix_ssl.ssl_trusted_certificate)
|
|
if not pl_path.exists(cert_path) then
|
|
util.die("certificate path", cert_path, "doesn't exist\n")
|
|
end
|
|
apisix_ssl.ssl_trusted_certificate = cert_path
|
|
end
|
|
end
|
|
|
|
replace_by_reserved_env_vars(default_conf)
|
|
|
|
return default_conf
|
|
end
|
|
|
|
|
|
return _M
|