feat(services): считать сервис живым при одном OK и писать историю failover
quality / commitlint (push) Skipped
quality / changes (push) Successful in 11s
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / web (push) Successful in 1m11s
quality / api (push) Successful in 1m1s
CD / quality (push) Successful in 2m30s
CD / publish (push) Successful in 2m4s
quality / commitlint (push) Skipped
quality / changes (push) Successful in 11s
quality / docker-check (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / web (push) Successful in 1m11s
quality / api (push) Successful in 1m1s
CD / quality (push) Successful in 2m30s
CD / publish (push) Successful in 2m4s
Сервис up, если жив хотя бы один IP. Failover показывает Down по FQDN и журнал add/remove A-записей. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createMemoryDb, repos, runMigrations } from "@cfdm/db";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { failoverARecordDiff } from "../src/services/service-config-service.js";
|
||||
|
||||
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
|
||||
const config = loadConfig();
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/auth/login",
|
||||
payload: { username: config.adminUsername, password: "admin" },
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const { token } = res.json() as { token: string };
|
||||
return { authorization: `Bearer ${token}` };
|
||||
}
|
||||
|
||||
describe("failoverARecordDiff", () => {
|
||||
it("diffs added and removed A contents", () => {
|
||||
expect(
|
||||
failoverARecordDiff(
|
||||
["130.49.213.153", "93.115.203.183"],
|
||||
["93.115.203.183"],
|
||||
),
|
||||
).toEqual({
|
||||
added: [],
|
||||
removed: ["130.49.213.153"],
|
||||
});
|
||||
expect(failoverARecordDiff(["10.0.0.1"], ["10.0.0.1", "10.0.0.2"])).toEqual({
|
||||
added: ["10.0.0.2"],
|
||||
removed: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /services/:id/failover-log", () => {
|
||||
it("returns add/remove rows for the service", async () => {
|
||||
const app = await buildApp({
|
||||
config: { ...loadConfig(), staticDir: null },
|
||||
memory: true,
|
||||
});
|
||||
const headers = await authHeaders(app);
|
||||
|
||||
const domain = repos.createDomain(app.db, null, "rkns.top", "zone-1");
|
||||
const service = repos.createService(app.db, "MSK Hip", "msk-hip");
|
||||
const binding = repos.insertBinding(app.db, domain.id, service.id, "gt", null);
|
||||
|
||||
repos.insertFailoverLog(app.db, {
|
||||
serviceId: service.id,
|
||||
bindingId: binding.id,
|
||||
fqdn: "gt.rkns.top",
|
||||
entries: [
|
||||
{ ip: "130.49.213.153", action: "removed" },
|
||||
{ ip: "93.115.203.183", action: "added" },
|
||||
],
|
||||
});
|
||||
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/v1/services/${service.id}/failover-log`,
|
||||
headers,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json() as {
|
||||
items: Array<{ ip: string; fqdn: string; action: string }>;
|
||||
};
|
||||
expect(body.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ip: "130.49.213.153",
|
||||
fqdn: "gt.rkns.top",
|
||||
action: "removed",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ip: "93.115.203.183",
|
||||
fqdn: "gt.rkns.top",
|
||||
action: "added",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("failover_log table", () => {
|
||||
it("lists newest first", () => {
|
||||
const { db, sqlite } = createMemoryDb();
|
||||
runMigrations(sqlite);
|
||||
|
||||
const domain = repos.createDomain(db, null, "example.com", "zone-1");
|
||||
const service = repos.createService(db, "Panel", "panel");
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "panel", null);
|
||||
|
||||
repos.insertFailoverLog(db, {
|
||||
serviceId: service.id,
|
||||
bindingId: binding.id,
|
||||
fqdn: "panel.example.com",
|
||||
entries: [{ ip: "1.1.1.1", action: "removed" }],
|
||||
});
|
||||
repos.insertFailoverLog(db, {
|
||||
serviceId: service.id,
|
||||
bindingId: binding.id,
|
||||
fqdn: "panel.example.com",
|
||||
entries: [{ ip: "1.1.1.1", action: "added" }],
|
||||
});
|
||||
|
||||
const rows = repos.listFailoverLogForService(db, service.id);
|
||||
expect(rows.map((row) => row.action)).toEqual(["added", "removed"]);
|
||||
});
|
||||
});
|
||||
@@ -397,4 +397,43 @@ describe("CNAME health mapped onto service IPs", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("getView is up when any binding IP is up", async () => {
|
||||
const { createMemoryDb, repos, runMigrations } = await import("@cfdm/db");
|
||||
const { getView } = await import("../src/services/service-config-service.js");
|
||||
const { db, sqlite } = createMemoryDb();
|
||||
runMigrations(sqlite);
|
||||
|
||||
const domain = repos.createDomain(db, null, "rkns.top", "zone-id");
|
||||
const service = repos.createService(db, "MSK Hip", "msk-hip");
|
||||
repos.replaceServiceIps(db, service.id, ["10.0.0.1", "10.0.0.2"]);
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
|
||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||
{ ip: "10.0.0.1", weight: 1, priority: 1 },
|
||||
{ ip: "10.0.0.2", weight: 1, priority: 1 },
|
||||
]);
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
binding.id,
|
||||
"10.0.0.1",
|
||||
"up",
|
||||
12,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
repos.upsertIpHealthStatus(
|
||||
db,
|
||||
"binding",
|
||||
binding.id,
|
||||
"10.0.0.2",
|
||||
"down",
|
||||
null,
|
||||
5,
|
||||
"timeout",
|
||||
);
|
||||
|
||||
const view = await getView(db, service.id);
|
||||
expect(view.health_status).toBe("up");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user