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:
2025-09-04 09:42:47 -05:00
parent f7bae09f22
commit 54cc5f7308
1608 changed files with 388342 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
--
-- 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 os = os
local ngx_re = require("ngx.re")
local core = require("apisix.core")
local util = require("apisix.plugins.proxy-cache.util")
local _M = {}
local function disk_cache_purge(conf, ctx)
local cache_zone_info = ngx_re.split(ctx.var.upstream_cache_zone_info, ",")
local filename = util.generate_cache_filename(cache_zone_info[1], cache_zone_info[2],
ctx.var.upstream_cache_key)
if util.file_exists(filename) then
os.remove(filename)
return nil
end
return "Not found"
end
function _M.access(conf, ctx)
ctx.var.upstream_cache_zone = conf.cache_zone
if ctx.var.request_method == "PURGE" then
local err = disk_cache_purge(conf, ctx)
if err ~= nil then
return 404
end
return 200
end
if conf.cache_bypass ~= nil then
local value = util.generate_complex_value(conf.cache_bypass, ctx)
ctx.var.upstream_cache_bypass = value
core.log.info("proxy-cache cache bypass value:", value)
end
if not util.match_method(conf, ctx) then
ctx.var.upstream_cache_bypass = "1"
core.log.info("proxy-cache cache bypass method: ", ctx.var.request_method)
end
end
function _M.header_filter(conf, ctx)
local no_cache = "1"
if util.match_method(conf, ctx) and util.match_status(conf, ctx) then
no_cache = "0"
end
if conf.no_cache ~= nil then
local value = util.generate_complex_value(conf.no_cache, ctx)
core.log.info("proxy-cache no-cache value:", value)
if value ~= nil and value ~= "" and value ~= "0" then
no_cache = "1"
end
end
local upstream_hdr_cache_control
local upstream_hdr_expires
if conf.hide_cache_headers == true then
upstream_hdr_cache_control = ""
upstream_hdr_expires = ""
else
upstream_hdr_cache_control = ctx.var.upstream_http_cache_control
upstream_hdr_expires = ctx.var.upstream_http_expires
end
core.response.set_header("Cache-Control", upstream_hdr_cache_control,
"Expires", upstream_hdr_expires,
"Apisix-Cache-Status", ctx.var.upstream_cache_status)
ctx.var.upstream_no_cache = no_cache
core.log.info("proxy-cache no cache:", no_cache)
end
return _M

View File

@@ -0,0 +1,198 @@
--
-- 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 memory_handler = require("apisix.plugins.proxy-cache.memory_handler")
local disk_handler = require("apisix.plugins.proxy-cache.disk_handler")
local util = require("apisix.plugins.proxy-cache.util")
local core = require("apisix.core")
local ipairs = ipairs
local plugin_name = "proxy-cache"
local STRATEGY_DISK = "disk"
local STRATEGY_MEMORY = "memory"
local DEFAULT_CACHE_ZONE = "disk_cache_one"
local schema = {
type = "object",
properties = {
cache_zone = {
type = "string",
minLength = 1,
maxLength = 100,
default = DEFAULT_CACHE_ZONE,
},
cache_strategy = {
type = "string",
enum = {STRATEGY_DISK, STRATEGY_MEMORY},
default = STRATEGY_DISK,
},
cache_key = {
type = "array",
minItems = 1,
items = {
description = "a key for caching",
type = "string",
pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]],
},
default = {"$host", "$request_uri"}
},
cache_http_status = {
type = "array",
minItems = 1,
items = {
description = "http response status",
type = "integer",
minimum = 200,
maximum = 599,
},
uniqueItems = true,
default = {200, 301, 404},
},
cache_method = {
type = "array",
minItems = 1,
items = {
description = "supported http method",
type = "string",
enum = {"GET", "POST", "HEAD"},
},
uniqueItems = true,
default = {"GET", "HEAD"},
},
hide_cache_headers = {
type = "boolean",
default = false,
},
cache_control = {
type = "boolean",
default = false,
},
cache_bypass = {
type = "array",
minItems = 1,
items = {
type = "string",
pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]]
},
},
no_cache = {
type = "array",
minItems = 1,
items = {
type = "string",
pattern = [[(^[^\$].+$|^\$[0-9a-zA-Z_]+$)]]
},
},
cache_ttl = {
type = "integer",
minimum = 1,
default = 300,
},
},
}
local _M = {
version = 0.2,
priority = 1085,
name = plugin_name,
schema = schema,
}
function _M.check_schema(conf)
local ok, err = core.schema.check(schema, conf)
if not ok then
return false, err
end
for _, key in ipairs(conf.cache_key) do
if key == "$request_method" then
return false, "cache_key variable " .. key .. " unsupported"
end
end
local found = false
local local_conf = core.config.local_conf()
if local_conf.apisix.proxy_cache then
local err = "cache_zone " .. conf.cache_zone .. " not found"
for _, cache in ipairs(local_conf.apisix.proxy_cache.zones) do
-- cache_zone passed in plugin config matched one of the proxy_cache zones
if cache.name == conf.cache_zone then
-- check for the mismatch between cache_strategy and corresponding cache zone
if (conf.cache_strategy == STRATEGY_MEMORY and cache.disk_path) or
(conf.cache_strategy == STRATEGY_DISK and not cache.disk_path) then
err = "invalid or empty cache_zone for cache_strategy: "..conf.cache_strategy
else
found = true
end
break
end
end
if found == false then
return false, err
end
end
return true
end
function _M.access(conf, ctx)
core.log.info("proxy-cache plugin access phase, conf: ", core.json.delay_encode(conf))
local value = util.generate_complex_value(conf.cache_key, ctx)
ctx.var.upstream_cache_key = value
core.log.info("proxy-cache cache key value:", value)
local handler
if conf.cache_strategy == STRATEGY_MEMORY then
handler = memory_handler
else
handler = disk_handler
end
return handler.access(conf, ctx)
end
function _M.header_filter(conf, ctx)
core.log.info("proxy-cache plugin header filter phase, conf: ", core.json.delay_encode(conf))
local handler
if conf.cache_strategy == STRATEGY_MEMORY then
handler = memory_handler
else
handler = disk_handler
end
handler.header_filter(conf, ctx)
end
function _M.body_filter(conf, ctx)
core.log.info("proxy-cache plugin body filter phase, conf: ", core.json.delay_encode(conf))
if conf.cache_strategy == STRATEGY_MEMORY then
memory_handler.body_filter(conf, ctx)
end
end
return _M

View File

@@ -0,0 +1,84 @@
--
-- 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 ngx_shared = ngx.shared
local setmetatable = setmetatable
local core = require("apisix.core")
local _M = {}
local mt = { __index = _M }
function _M.new(opts)
return setmetatable({
dict = ngx_shared[opts.shdict_name],
}, mt)
end
function _M:set(key, obj, ttl)
if self.dict == nil then
return nil, "invalid cache_zone provided"
end
local obj_json = core.json.encode(obj)
if not obj_json then
return nil, "could not encode object"
end
local succ, err = self.dict:set(key, obj_json, ttl)
return succ and obj_json or nil, err
end
function _M:get(key)
if self.dict == nil then
return nil, "invalid cache_zone provided"
end
-- If the key does not exist or has expired, then res_json will be nil.
local res_json, err, stale = self.dict:get_stale(key)
if not res_json then
if not err then
return nil, "not found"
else
return nil, err
end
end
if stale then
return nil, "expired"
end
local res_obj, err = core.json.decode(res_json)
if not res_obj then
return nil, err
end
return res_obj, nil
end
function _M:purge(key)
if self.dict == nil then
return nil, "invalid cache_zone provided"
end
self.dict:delete(key)
end
return _M

View File

@@ -0,0 +1,332 @@
--
-- 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 memory_strategy = require("apisix.plugins.proxy-cache.memory").new
local util = require("apisix.plugins.proxy-cache.util")
local core = require("apisix.core")
local tab_new = require("table.new")
local ngx_re_gmatch = ngx.re.gmatch
local ngx_re_match = ngx.re.match
local parse_http_time = ngx.parse_http_time
local concat = table.concat
local lower = string.lower
local floor = math.floor
local tostring = tostring
local tonumber = tonumber
local ngx = ngx
local type = type
local pairs = pairs
local time = ngx.now
local max = math.max
local CACHE_VERSION = 1
local _M = {}
-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
-- note content-length & apisix-cache-status are not strictly
-- hop-by-hop but we will be adjusting it here anyhow
local hop_by_hop_headers = {
["connection"] = true,
["keep-alive"] = true,
["proxy-authenticate"] = true,
["proxy-authorization"] = true,
["te"] = true,
["trailers"] = true,
["transfer-encoding"] = true,
["upgrade"] = true,
["content-length"] = true,
["apisix-cache-status"] = true,
}
local function include_cache_header(header)
local n_header = lower(header)
if n_header == "expires" or n_header == "cache-control" then
return true
end
return false
end
local function overwritable_header(header)
local n_header = lower(header)
return not hop_by_hop_headers[n_header]
and not ngx_re_match(n_header, "ratelimit-remaining")
end
-- The following format can accept:
-- Cache-Control: no-cache
-- Cache-Control: no-store
-- Cache-Control: max-age=3600
-- Cache-Control: max-stale=3600
-- Cache-Control: min-fresh=3600
-- Cache-Control: private, max-age=600
-- Cache-Control: public, max-age=31536000
-- Refer to: https://www.holisticseo.digital/pagespeed/cache-control/
local function parse_directive_header(h)
if not h then
return {}
end
if type(h) == "table" then
h = concat(h, ", ")
end
local t = {}
local res = tab_new(3, 0)
local iter = ngx_re_gmatch(h, "([^,]+)", "oj")
local m = iter()
while m do
local _, err = ngx_re_match(m[0], [[^\s*([^=]+)(?:=(.+))?]],
"oj", nil, res)
if err then
core.log.error(err)
end
-- store the directive token as a numeric value if it looks like a number;
-- otherwise, store the string value. for directives without token, we just
-- set the key to true
t[lower(res[1])] = tonumber(res[2]) or res[2] or true
m = iter()
end
return t
end
local function parse_resource_ttl(ctx, cc)
local max_age = cc["s-maxage"] or cc["max-age"]
if not max_age then
local expires = ctx.var.upstream_http_expires
-- if multiple Expires headers are present, last one wins
if type(expires) == "table" then
expires = expires[#expires]
end
local exp_time = parse_http_time(tostring(expires))
if exp_time then
max_age = exp_time - time()
end
end
return max_age and max(max_age, 0) or 0
end
local function cacheable_request(conf, ctx, cc)
if not util.match_method(conf, ctx) then
return false, "MISS"
end
if conf.cache_bypass ~= nil then
local value = util.generate_complex_value(conf.cache_bypass, ctx)
core.log.info("proxy-cache cache bypass value:", value)
if value ~= nil and value ~= "" and value ~= "0" then
return false, "BYPASS"
end
end
if conf.cache_control and (cc["no-store"] or cc["no-cache"]) then
return false, "BYPASS"
end
return true, ""
end
local function cacheable_response(conf, ctx, cc)
if not util.match_status(conf, ctx) then
return false
end
if conf.no_cache ~= nil then
local value = util.generate_complex_value(conf.no_cache, ctx)
core.log.info("proxy-cache no-cache value:", value)
if value ~= nil and value ~= "" and value ~= "0" then
return false
end
end
if conf.cache_control and (cc["private"] or cc["no-store"] or cc["no-cache"]) then
return false
end
if conf.cache_control and parse_resource_ttl(ctx, cc) <= 0 then
return false
end
return true
end
function _M.access(conf, ctx)
local cc = parse_directive_header(ctx.var.http_cache_control)
if ctx.var.request_method ~= "PURGE" then
local ret, msg = cacheable_request(conf, ctx, cc)
if not ret then
core.response.set_header("Apisix-Cache-Status", msg)
return
end
end
if not ctx.cache then
ctx.cache = {
memory = memory_strategy({shdict_name = conf.cache_zone}),
hit = false,
ttl = 0,
}
end
local res, err = ctx.cache.memory:get(ctx.var.upstream_cache_key)
if ctx.var.request_method == "PURGE" then
if err == "not found" then
return 404
end
ctx.cache.memory:purge(ctx.var.upstream_cache_key)
ctx.cache = nil
return 200
end
if err then
if err == "expired" then
core.response.set_header("Apisix-Cache-Status", "EXPIRED")
elseif err ~= "not found" then
core.response.set_header("Apisix-Cache-Status", "MISS")
core.log.error("failed to get from cache, err: ", err)
elseif conf.cache_control and cc["only-if-cached"] then
core.response.set_header("Apisix-Cache-Status", "MISS")
return 504
else
core.response.set_header("Apisix-Cache-Status", "MISS")
end
return
end
if res.version ~= CACHE_VERSION then
core.log.warn("cache format mismatch, purging ", ctx.var.upstream_cache_key)
core.response.set_header("Apisix-Cache-Status", "BYPASS")
ctx.cache.memory:purge(ctx.var.upstream_cache_key)
return
end
if conf.cache_control then
if cc["max-age"] and time() - res.timestamp > cc["max-age"] then
core.response.set_header("Apisix-Cache-Status", "STALE")
return
end
if cc["max-stale"] and time() - res.timestamp - res.ttl > cc["max-stale"] then
core.response.set_header("Apisix-Cache-Status", "STALE")
return
end
if cc["min-fresh"] and res.ttl - (time() - res.timestamp) < cc["min-fresh"] then
core.response.set_header("Apisix-Cache-Status", "STALE")
return
end
else
if time() - res.timestamp > res.ttl then
core.response.set_header("Apisix-Cache-Status", "STALE")
return
end
end
ctx.cache.hit = true
for key, value in pairs(res.headers) do
if conf.hide_cache_headers == true and include_cache_header(key) then
core.response.set_header(key, "")
elseif overwritable_header(key) then
core.response.set_header(key, value)
end
end
core.response.set_header("Age", floor(time() - res.timestamp))
core.response.set_header("Apisix-Cache-Status", "HIT")
return res.status, res.body
end
function _M.header_filter(conf, ctx)
local cache = ctx.cache
if not cache or cache.hit then
return
end
local res_headers = ngx.resp.get_headers(0, true)
for key in pairs(res_headers) do
if conf.hide_cache_headers == true and include_cache_header(key) then
core.response.set_header(key, "")
end
end
local cc = parse_directive_header(ctx.var.upstream_http_cache_control)
if cacheable_response(conf, ctx, cc) then
cache.res_headers = res_headers
cache.ttl = conf.cache_control and parse_resource_ttl(ctx, cc) or conf.cache_ttl
else
ctx.cache = nil
end
end
function _M.body_filter(conf, ctx)
local cache = ctx.cache
if not cache or cache.hit then
return
end
local res_body = core.response.hold_body_chunk(ctx, true)
if not res_body then
return
end
local res = {
status = ngx.status,
body = res_body,
body_len = #res_body,
headers = cache.res_headers,
ttl = cache.ttl,
timestamp = time(),
version = CACHE_VERSION,
}
local res, err = cache.memory:set(ctx.var.upstream_cache_key, res, cache.ttl)
if not res then
core.log.error("failed to set cache, err: ", err)
end
end
return _M

View File

@@ -0,0 +1,102 @@
--
-- 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 core = require("apisix.core")
local ngx_re = require("ngx.re")
local tab_concat = table.concat
local string = string
local io_open = io.open
local io_close = io.close
local ngx = ngx
local ipairs = ipairs
local pairs = pairs
local tonumber = tonumber
local _M = {}
local tmp = {}
function _M.generate_complex_value(data, ctx)
core.table.clear(tmp)
core.log.info("proxy-cache complex value: ", core.json.delay_encode(data))
for i, value in ipairs(data) do
core.log.info("proxy-cache complex value index-", i, ": ", value)
if string.byte(value, 1, 1) == string.byte('$') then
tmp[i] = ctx.var[string.sub(value, 2)] or ""
else
tmp[i] = value
end
end
return tab_concat(tmp, "")
end
-- check whether the request method match the user defined.
function _M.match_method(conf, ctx)
for _, method in ipairs(conf.cache_method) do
if method == ctx.var.request_method then
return true
end
end
return false
end
-- check whether the response status match the user defined.
function _M.match_status(conf, ctx)
for _, status in ipairs(conf.cache_http_status) do
if status == ngx.status then
return true
end
end
return false
end
function _M.file_exists(name)
local f = io_open(name, "r")
if f ~= nil then
io_close(f)
return true
end
return false
end
function _M.generate_cache_filename(cache_path, cache_levels, cache_key)
local md5sum = ngx.md5(cache_key)
local levels = ngx_re.split(cache_levels, ":")
local filename = ""
local index = #md5sum
for k, v in pairs(levels) do
local length = tonumber(v)
index = index - length
filename = filename .. md5sum:sub(index+1, index+length) .. "/"
end
if cache_path:sub(-1) ~= "/" then
cache_path = cache_path .. "/"
end
filename = cache_path .. filename .. md5sum
return filename
end
return _M