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:
405
CloudronPackages/APISIX/apisix-source/apisix/cli/etcd.lua
Normal file
405
CloudronPackages/APISIX/apisix-source/apisix/cli/etcd.lua
Normal file
@@ -0,0 +1,405 @@
|
||||
--
|
||||
-- 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 base64_encode = require("base64").encode
|
||||
local dkjson = require("dkjson")
|
||||
local constants = require("apisix.constants")
|
||||
local util = require("apisix.cli.util")
|
||||
local file = require("apisix.cli.file")
|
||||
local http = require("socket.http")
|
||||
local https = require("ssl.https")
|
||||
local ltn12 = require("ltn12")
|
||||
|
||||
local type = type
|
||||
local ipairs = ipairs
|
||||
local pairs = pairs
|
||||
local print = print
|
||||
local tonumber = tonumber
|
||||
local str_format = string.format
|
||||
local str_sub = string.sub
|
||||
local table_concat = table.concat
|
||||
local table_insert = table.insert
|
||||
local io_stderr = io.stderr
|
||||
|
||||
local _M = {}
|
||||
|
||||
-- Timeout for all I/O operations
|
||||
http.TIMEOUT = 3
|
||||
|
||||
local function parse_semantic_version(ver)
|
||||
local errmsg = "invalid semantic version: " .. ver
|
||||
|
||||
local parts = util.split(ver, "-")
|
||||
if #parts > 2 then
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
if #parts == 2 then
|
||||
ver = parts[1]
|
||||
end
|
||||
|
||||
local fields = util.split(ver, ".")
|
||||
if #fields ~= 3 then
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
local major = tonumber(fields[1])
|
||||
local minor = tonumber(fields[2])
|
||||
local patch = tonumber(fields[3])
|
||||
|
||||
if not (major and minor and patch) then
|
||||
return nil, errmsg
|
||||
end
|
||||
|
||||
return {
|
||||
major = major,
|
||||
minor = minor,
|
||||
patch = patch,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
local function compare_semantic_version(v1, v2)
|
||||
local ver1, err = parse_semantic_version(v1)
|
||||
if not ver1 then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local ver2, err = parse_semantic_version(v2)
|
||||
if not ver2 then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
if ver1.major ~= ver2.major then
|
||||
return ver1.major < ver2.major
|
||||
end
|
||||
|
||||
if ver1.minor ~= ver2.minor then
|
||||
return ver1.minor < ver2.minor
|
||||
end
|
||||
|
||||
return ver1.patch < ver2.patch
|
||||
end
|
||||
|
||||
|
||||
local function request(url, yaml_conf)
|
||||
local response_body = {}
|
||||
local single_request = false
|
||||
if type(url) == "string" then
|
||||
url = {
|
||||
url = url,
|
||||
method = "GET",
|
||||
sink = ltn12.sink.table(response_body),
|
||||
}
|
||||
single_request = true
|
||||
end
|
||||
|
||||
local res, code
|
||||
|
||||
if str_sub(url.url, 1, 8) == "https://" then
|
||||
local verify = "peer"
|
||||
if yaml_conf.etcd.tls then
|
||||
local cfg = yaml_conf.etcd.tls
|
||||
|
||||
if cfg.verify == false then
|
||||
verify = "none"
|
||||
end
|
||||
|
||||
url.certificate = cfg.cert
|
||||
url.key = cfg.key
|
||||
|
||||
local apisix_ssl = yaml_conf.apisix.ssl
|
||||
if apisix_ssl and apisix_ssl.ssl_trusted_certificate then
|
||||
url.cafile = apisix_ssl.ssl_trusted_certificate
|
||||
end
|
||||
end
|
||||
|
||||
url.verify = verify
|
||||
res, code = https.request(url)
|
||||
else
|
||||
|
||||
res, code = http.request(url)
|
||||
end
|
||||
|
||||
-- In case of failure, request returns nil followed by an error message.
|
||||
-- Else the first return value is the response body
|
||||
-- and followed by the response status code.
|
||||
if single_request and res ~= nil then
|
||||
return table_concat(response_body), code
|
||||
end
|
||||
|
||||
return res, code
|
||||
end
|
||||
|
||||
|
||||
local function prepare_dirs_via_http(yaml_conf, args, index, host, host_count)
|
||||
local is_success = true
|
||||
|
||||
local errmsg
|
||||
local auth_token
|
||||
local user = yaml_conf.etcd.user
|
||||
local password = yaml_conf.etcd.password
|
||||
if user and password then
|
||||
local auth_url = host .. "/v3/auth/authenticate"
|
||||
local json_auth = {
|
||||
name = user,
|
||||
password = password
|
||||
}
|
||||
|
||||
local post_json_auth = dkjson.encode(json_auth)
|
||||
local response_body = {}
|
||||
|
||||
local res, err
|
||||
local retry_time = 0
|
||||
while retry_time < 2 do
|
||||
res, err = request({
|
||||
url = auth_url,
|
||||
method = "POST",
|
||||
source = ltn12.source.string(post_json_auth),
|
||||
sink = ltn12.sink.table(response_body),
|
||||
headers = {
|
||||
["Content-Length"] = #post_json_auth
|
||||
}
|
||||
}, yaml_conf)
|
||||
-- In case of failure, request returns nil followed by an error message.
|
||||
-- Else the first return value is just the number 1
|
||||
-- and followed by the response status code.
|
||||
if res then
|
||||
break
|
||||
end
|
||||
retry_time = retry_time + 1
|
||||
print(str_format("Warning! Request etcd endpoint \'%s\' error, %s, retry time=%s",
|
||||
auth_url, err, retry_time))
|
||||
end
|
||||
|
||||
if not res then
|
||||
errmsg = str_format("request etcd endpoint \"%s\" error, %s\n", auth_url, err)
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
local res_auth = table_concat(response_body)
|
||||
local body_auth, _, err_auth = dkjson.decode(res_auth)
|
||||
if err_auth or (body_auth and not body_auth["token"]) then
|
||||
errmsg = str_format("got malformed auth message: \"%s\" from etcd \"%s\"\n",
|
||||
res_auth, auth_url)
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
auth_token = body_auth.token
|
||||
end
|
||||
|
||||
|
||||
local dirs = {}
|
||||
for name in pairs(constants.HTTP_ETCD_DIRECTORY) do
|
||||
dirs[name] = true
|
||||
end
|
||||
for name in pairs(constants.STREAM_ETCD_DIRECTORY) do
|
||||
dirs[name] = true
|
||||
end
|
||||
|
||||
for dir_name in pairs(dirs) do
|
||||
local key = (yaml_conf.etcd.prefix or "") .. dir_name .. "/"
|
||||
|
||||
local put_url = host .. "/v3/kv/put"
|
||||
local post_json = '{"value":"' .. base64_encode("init_dir")
|
||||
.. '", "key":"' .. base64_encode(key) .. '"}'
|
||||
local response_body = {}
|
||||
local headers = {["Content-Length"] = #post_json}
|
||||
if auth_token then
|
||||
headers["Authorization"] = auth_token
|
||||
end
|
||||
|
||||
local res, err
|
||||
local retry_time = 0
|
||||
while retry_time < 2 do
|
||||
res, err = request({
|
||||
url = put_url,
|
||||
method = "POST",
|
||||
source = ltn12.source.string(post_json),
|
||||
sink = ltn12.sink.table(response_body),
|
||||
headers = headers
|
||||
}, yaml_conf)
|
||||
retry_time = retry_time + 1
|
||||
if res then
|
||||
break
|
||||
end
|
||||
print(str_format("Warning! Request etcd endpoint \'%s\' error, %s, retry time=%s",
|
||||
put_url, err, retry_time))
|
||||
end
|
||||
|
||||
if not res then
|
||||
errmsg = str_format("request etcd endpoint \"%s\" error, %s\n", put_url, err)
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
local res_put = table_concat(response_body)
|
||||
if res_put:find("404 page not found", 1, true) then
|
||||
errmsg = str_format("gRPC gateway is not enabled in etcd cluster \"%s\",",
|
||||
"which is required by Apache APISIX\n")
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
if res_put:find("CommonName of client sending a request against gateway", 1, true) then
|
||||
errmsg = str_format("etcd \"client-cert-auth\" cannot be used with gRPC-gateway, "
|
||||
.. "please configure the etcd username and password "
|
||||
.. "in configuration file\n")
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
if res_put:find("error", 1, true) then
|
||||
is_success = false
|
||||
if (index == host_count) then
|
||||
errmsg = str_format("got malformed key-put message: \"%s\" from etcd \"%s\"\n",
|
||||
res_put, put_url)
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
if args and args["verbose"] then
|
||||
print(res_put)
|
||||
end
|
||||
end
|
||||
|
||||
return is_success
|
||||
end
|
||||
|
||||
|
||||
local function prepare_dirs(yaml_conf, args, index, host, host_count)
|
||||
return prepare_dirs_via_http(yaml_conf, args, index, host, host_count)
|
||||
end
|
||||
|
||||
|
||||
function _M.init(env, args)
|
||||
-- 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)
|
||||
end
|
||||
|
||||
if not yaml_conf.apisix then
|
||||
util.die("failed to read `apisix` field from yaml file when init etcd")
|
||||
end
|
||||
|
||||
if yaml_conf.deployment.config_provider ~= "etcd" then
|
||||
return true
|
||||
end
|
||||
|
||||
if not yaml_conf.etcd then
|
||||
util.die("failed to read `etcd` field from yaml file when init etcd")
|
||||
end
|
||||
|
||||
-- convert old single etcd config to multiple etcd config
|
||||
if type(yaml_conf.etcd.host) == "string" then
|
||||
yaml_conf.etcd.host = {yaml_conf.etcd.host}
|
||||
end
|
||||
|
||||
local host_count = #(yaml_conf.etcd.host)
|
||||
local scheme
|
||||
for i = 1, host_count do
|
||||
local host = yaml_conf.etcd.host[i]
|
||||
local fields = util.split(host, "://")
|
||||
if not fields then
|
||||
util.die("malformed etcd endpoint: ", host, "\n")
|
||||
end
|
||||
|
||||
if not scheme then
|
||||
scheme = fields[1]
|
||||
elseif scheme ~= fields[1] then
|
||||
print([[WARNING: mixed protocols among etcd endpoints]])
|
||||
end
|
||||
end
|
||||
|
||||
-- check the etcd cluster version
|
||||
local etcd_healthy_hosts = {}
|
||||
for index, host in ipairs(yaml_conf.etcd.host) do
|
||||
local version_url = host .. "/version"
|
||||
local errmsg
|
||||
|
||||
local res, err
|
||||
local retry_time = 0
|
||||
|
||||
local etcd = yaml_conf.etcd
|
||||
local max_retry = tonumber(etcd.startup_retry) or 2
|
||||
while retry_time < max_retry do
|
||||
res, err = request(version_url, yaml_conf)
|
||||
-- In case of failure, request returns nil followed by an error message.
|
||||
-- Else the first return value is the response body
|
||||
-- and followed by the response status code.
|
||||
if res then
|
||||
break
|
||||
end
|
||||
retry_time = retry_time + 1
|
||||
print(str_format("Warning! Request etcd endpoint \'%s\' error, %s, retry time=%s",
|
||||
version_url, err, retry_time))
|
||||
end
|
||||
|
||||
if res then
|
||||
local body, _, err = dkjson.decode(res)
|
||||
if err or (body and not body["etcdcluster"]) then
|
||||
errmsg = str_format("got malformed version message: \"%s\" from etcd \"%s\"\n", res,
|
||||
version_url)
|
||||
util.die(errmsg)
|
||||
end
|
||||
|
||||
local cluster_version = body["etcdcluster"]
|
||||
if compare_semantic_version(cluster_version, env.min_etcd_version) then
|
||||
util.die("etcd cluster version ", cluster_version,
|
||||
" is less than the required version ", env.min_etcd_version,
|
||||
", please upgrade your etcd cluster\n")
|
||||
end
|
||||
|
||||
table_insert(etcd_healthy_hosts, host)
|
||||
else
|
||||
io_stderr:write(str_format("request etcd endpoint \'%s\' error, %s\n", version_url,
|
||||
err))
|
||||
end
|
||||
end
|
||||
|
||||
if #etcd_healthy_hosts <= 0 then
|
||||
util.die("all etcd nodes are unavailable\n")
|
||||
end
|
||||
|
||||
if (#etcd_healthy_hosts / host_count * 100) <= 50 then
|
||||
util.die("the etcd cluster needs at least 50% and above healthy nodes\n")
|
||||
end
|
||||
|
||||
-- access from the data plane to etcd should be read-only.
|
||||
-- data plane writes to etcd may cause security issues.
|
||||
if yaml_conf.deployment.role == "data_plane" then
|
||||
print("access from the data plane to etcd should be read-only, "
|
||||
.."skip initializing the data of etcd")
|
||||
return true
|
||||
end
|
||||
|
||||
print("trying to initialize the data of etcd")
|
||||
local etcd_ok = false
|
||||
for index, host in ipairs(etcd_healthy_hosts) do
|
||||
if prepare_dirs(yaml_conf, args, index, host, host_count) then
|
||||
etcd_ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not etcd_ok then
|
||||
util.die("none of the configured etcd works well\n")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return _M
|
Reference in New Issue
Block a user