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,442 @@
/*
* 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.
*/
import axios from "axios";
import YAML from "yaml";
const ENDPOINT = "/apisix/admin/configs";
const clientConfig = {
baseURL: "http://localhost:1984",
headers: {
"X-API-KEY": "edd1c9f034335f136f87ad84b625c8f1",
},
};
const config1 = {
routes: [
{
id: "r1",
uri: "/r1",
upstream: {
nodes: { "127.0.0.1:1980": 1 },
type: "roundrobin",
},
plugins: { "proxy-rewrite": { uri: "/hello" } },
},
],
};
const config2 = {
routes: [
{
id: "r2",
uri: "/r2",
upstream: {
nodes: { "127.0.0.1:1980": 1 },
type: "roundrobin",
},
plugins: { "proxy-rewrite": { uri: "/hello" } },
},
],
};
const invalidConfVersionConfig1 = {
routes_conf_version: -1,
};
const invalidConfVersionConfig2 = {
routes_conf_version: "adc",
};
const routeWithModifiedIndex = {
routes: [
{
id: "r1",
uri: "/r1",
modifiedIndex: 1,
upstream: {
nodes: { "127.0.0.1:1980": 1 },
type: "roundrobin",
},
plugins: { "proxy-rewrite": { uri: "/hello" } },
},
],
};
const routeWithKeyAuth = {
routes: [
{
id: "r1",
uri: "/r1",
upstream: {
nodes: { "127.0.0.1:1980": 1 },
type: "roundrobin",
},
plugins: {
"proxy-rewrite": { uri: "/hello" },
"key-auth": {},
},
},
]
}
const consumerWithModifiedIndex = {
routes: routeWithKeyAuth.routes,
consumers: [
{
modifiedIndex: 10,
username: "jack",
plugins: {
"key-auth": {
key: "jack-key",
}
},
},
],
}
const credential1 = {
routes: routeWithKeyAuth.routes,
consumers: [
{
"username": "john_1"
},
{
"id": "john_1/credentials/john-a",
"plugins": {
"key-auth": {
"key": "auth-a"
}
}
},
{
"id": "john_1/credentials/john-b",
"plugins": {
"key-auth": {
"key": "auth-b"
}
}
}
]
}
describe("Admin - Standalone", () => {
const client = axios.create(clientConfig);
client.interceptors.response.use((response) => {
const contentType = response.headers["content-type"] || "";
if (
contentType.includes("application/yaml") &&
typeof response.data === "string" &&
response.config.responseType !== "text"
)
response.data = YAML.parse(response.data);
return response;
});
describe("Normal", () => {
it("dump empty config (default json format)", async () => {
const resp = await client.get(ENDPOINT);
expect(resp.status).toEqual(200);
expect(resp.data.routes_conf_version).toEqual(0);
expect(resp.data.ssls_conf_version).toEqual(0);
expect(resp.data.services_conf_version).toEqual(0);
expect(resp.data.upstreams_conf_version).toEqual(0);
expect(resp.data.consumers_conf_version).toEqual(0);
});
it("dump empty config (yaml format)", async () => {
const resp = await client.get(ENDPOINT, {
headers: { Accept: "application/yaml" },
});
expect(resp.status).toEqual(200);
expect(resp.headers["content-type"]).toEqual("application/yaml");
expect(resp.data.routes_conf_version).toEqual(0);
expect(resp.data.ssls_conf_version).toEqual(0);
expect(resp.data.services_conf_version).toEqual(0);
expect(resp.data.upstreams_conf_version).toEqual(0);
expect(resp.data.consumers_conf_version).toEqual(0);
});
it("update config (add routes, by json)", async () => {
const resp = await client.put(ENDPOINT, config1);
expect(resp.status).toEqual(202);
});
it("dump config (json format)", async () => {
const resp = await client.get(ENDPOINT);
expect(resp.status).toEqual(200);
expect(resp.data.routes_conf_version).toEqual(1);
expect(resp.data.ssls_conf_version).toEqual(1);
expect(resp.data.services_conf_version).toEqual(1);
expect(resp.data.upstreams_conf_version).toEqual(1);
expect(resp.data.consumers_conf_version).toEqual(1);
});
it("check default value", async () => {
const resp = await client.get(ENDPOINT);
expect(resp.status).toEqual(200);
expect(resp.data.routes).toEqual(config1.routes);
});
it("dump config (yaml format)", async () => {
const resp = await client.get(ENDPOINT, {
headers: { Accept: "application/yaml" },
responseType: 'text',
});
expect(resp.status).toEqual(200);
expect(resp.data).toContain("routes:")
expect(resp.data).toContain("id: r1")
expect(resp.data.startsWith('---')).toBe(false);
expect(resp.data.endsWith('...')).toBe(false);
});
it('check route "r1"', async () => {
const resp = await client.get("/r1");
expect(resp.status).toEqual(200);
expect(resp.data).toEqual("hello world\n");
});
it("update config (add routes, by yaml)", async () => {
const resp = await client.put(
ENDPOINT,
YAML.stringify(config2),
{
headers: { "Content-Type": "application/yaml" },
}
);
expect(resp.status).toEqual(202);
});
it("dump config (json format)", async () => {
const resp = await client.get(ENDPOINT);
expect(resp.status).toEqual(200);
expect(resp.data.routes_conf_version).toEqual(2);
expect(resp.data.ssls_conf_version).toEqual(2);
expect(resp.data.services_conf_version).toEqual(2);
expect(resp.data.upstreams_conf_version).toEqual(2);
expect(resp.data.consumers_conf_version).toEqual(2);
});
it('check route "r1"', () =>
expect(client.get("/r1")).rejects.toThrow(
"Request failed with status code 404"
));
it('check route "r2"', async () => {
const resp = await client.get("/r2");
expect(resp.status).toEqual(200);
expect(resp.data).toEqual("hello world\n");
});
it("update config (delete routes)", async () => {
const resp = await client.put(
ENDPOINT,
{},
{ params: { conf_version: 3 } }
);
expect(resp.status).toEqual(202);
});
it('check route "r2"', () =>
expect(client.get("/r2")).rejects.toThrow(
"Request failed with status code 404"
));
it("only set routes_conf_version", async () => {
const resp = await client.put(
ENDPOINT,
YAML.stringify({ routes_conf_version: 15 }),
{
headers: { "Content-Type": "application/yaml" },
});
expect(resp.status).toEqual(202);
const resp_1 = await client.get(ENDPOINT);
expect(resp_1.status).toEqual(200);
expect(resp_1.data.routes_conf_version).toEqual(15);
expect(resp_1.data.ssls_conf_version).toEqual(4);
expect(resp_1.data.services_conf_version).toEqual(4);
expect(resp_1.data.upstreams_conf_version).toEqual(4);
expect(resp_1.data.consumers_conf_version).toEqual(4);
const resp2 = await client.put(
ENDPOINT,
YAML.stringify({ routes_conf_version: 17 }),
{
headers: { "Content-Type": "application/yaml" },
});
expect(resp2.status).toEqual(202);
const resp2_1 = await client.get(ENDPOINT);
expect(resp2_1.status).toEqual(200);
expect(resp2_1.data.routes_conf_version).toEqual(17);
expect(resp2_1.data.ssls_conf_version).toEqual(5);
expect(resp2_1.data.services_conf_version).toEqual(5);
expect(resp2_1.data.upstreams_conf_version).toEqual(5);
expect(resp2_1.data.consumers_conf_version).toEqual(5);
});
it("control resource changes using modifiedIndex", async () => {
const c1 = structuredClone(routeWithModifiedIndex);
c1.routes[0].modifiedIndex = 1;
const c2 = structuredClone(c1);
c2.routes[0].uri = "/r2";
const c3 = structuredClone(c2);
c3.routes[0].modifiedIndex = 2;
// Update with c1
const resp = await client.put(ENDPOINT, c1);
expect(resp.status).toEqual(202);
// Check route /r1 exists
const resp_1 = await client.get("/r1");
expect(resp_1.status).toEqual(200);
// Update with c2
const resp2 = await client.put(ENDPOINT, c2);
expect(resp2.status).toEqual(202);
// Check route /r1 exists
// But it is not applied because the modifiedIndex is the same as the old value
const resp2_2 = await client.get("/r1");
expect(resp2_2.status).toEqual(200);
// Check route /r2 not exists
const resp2_1 = await client.get("/r2").catch((err) => err.response);
expect(resp2_1.status).toEqual(404);
// Update with c3
const resp3 = await client.put(ENDPOINT, c3);
expect(resp3.status).toEqual(202);
// Check route /r1 not exists
const resp3_1 = await client.get("/r1").catch((err) => err.response);
expect(resp3_1.status).toEqual(404);
// Check route /r2 exists
const resp3_2 = await client.get("/r2");
expect(resp3_2.status).toEqual(200);
});
it("apply consumer with modifiedIndex", async () => {
const resp = await client.put(ENDPOINT, consumerWithModifiedIndex);
expect(resp.status).toEqual(202);
const resp_1 = await client.get("/r1", { headers: { "apikey": "invalid-key" } }).catch((err) => err.response);
expect(resp_1.status).toEqual(401);
const resp_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } });
expect(resp_2.status).toEqual(200);
const updatedConsumer = structuredClone(consumerWithModifiedIndex);
// update key of key-auth plugin, but modifiedIndex is not changed
updatedConsumer.consumers[0].plugins["key-auth"] = { "key": "jack-key-updated" };
const resp2 = await client.put(ENDPOINT, updatedConsumer);
expect(resp2.status).toEqual(202);
const resp2_1 = await client.get("/r1", { headers: { "apikey": "jack-key-updated" } }).catch((err) => err.response);
expect(resp2_1.status).toEqual(401);
const resp2_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } });
expect(resp2_2.status).toEqual(200);
// update key of key-auth plugin, and modifiedIndex is changed
updatedConsumer.consumers[0].modifiedIndex++;
const resp3 = await client.put(ENDPOINT, updatedConsumer);
const resp3_1 = await client.get("/r1", { headers: { "apikey": "jack-key-updated" } });
expect(resp3_1.status).toEqual(200);
const resp3_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } }).catch((err) => err.response);
expect(resp3_2.status).toEqual(401);
});
it("apply consumer with credentials", async () => {
const resp = await client.put(ENDPOINT, credential1);
expect(resp.status).toEqual(202);
const resp_1 = await client.get("/r1", { headers: { "apikey": "auth-a" } });
expect(resp_1.status).toEqual(200);
const resp_2 = await client.get("/r1", { headers: { "apikey": "auth-b" } });
expect(resp_2.status).toEqual(200);
const resp_3 = await client.get("/r1", { headers: { "apikey": "invalid-key" } }).catch((err) => err.response);
expect(resp_3.status).toEqual(401);
});
});
describe("Exceptions", () => {
const clientException = axios.create({
...clientConfig,
validateStatus: () => true,
});
it("update config (lower conf_version)", async () => {
const resp = await clientException.put(
ENDPOINT,
{ routes_conf_version: 100 },
{ headers: { "Content-Type": "application/yaml" } }
);
const resp2 = await clientException.put(
ENDPOINT,
YAML.stringify(invalidConfVersionConfig1),
{ headers: { "Content-Type": "application/yaml" } }
);
expect(resp2.status).toEqual(400);
expect(resp2.data).toEqual({
error_msg:
"routes_conf_version must be greater than or equal to (100)",
});
});
it("update config (invalid conf_version)", async () => {
const resp = await clientException.put(
ENDPOINT,
YAML.stringify(invalidConfVersionConfig2),
{
headers: {
"Content-Type": "application/yaml",
},
}
);
expect(resp.status).toEqual(400);
expect(resp.data).toEqual({
error_msg: "routes_conf_version must be a number",
});
});
it("update config (invalid json format)", async () => {
const resp = await clientException.put(ENDPOINT, "{abcd", {
params: { conf_version: 4 },
});
expect(resp.status).toEqual(400);
expect(resp.data).toEqual({
error_msg:
"invalid request body: Expected object key string but found invalid token at character 2",
});
});
it("update config (not compliant with jsonschema)", async () => {
const data = structuredClone(config1);
(data.routes[0].uri as unknown) = 123;
const resp = await clientException.put(ENDPOINT, data);
expect(resp.status).toEqual(400);
expect(resp.data).toMatchObject({
error_msg:
'invalid routes at index 0, err: property "uri" validation failed: wrong type: expected string, got number',
});
});
it("update config (empty request body)", async () => {
const resp = await clientException.put(ENDPOINT, "");
expect(resp.status).toEqual(400);
expect(resp.data).toEqual({
error_msg: "invalid request body: empty request body",
});
});
});
});