fix: align factory probes with git-proxy routes
This commit is contained in:
parent
9df2929446
commit
2fadb8a428
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
deploy-webhook:
|
||||
url: http://77.42.112.29:8098/health
|
||||
url: http://77.42.112.29:8099/deploy
|
||||
provides: [deploy]
|
||||
required: true
|
||||
git-proxy:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export interface CyberPreflightOptions {
|
|||
const REQUIRED_ENDPOINTS = [
|
||||
["git_proxy_health", "http://127.0.0.1:8099/health"],
|
||||
["git_proxy_factory_status", "http://127.0.0.1:8099/factory-status"],
|
||||
["deploy_webhook_health", "http://127.0.0.1:8098/health"],
|
||||
["deploy_webhook_health", "http://127.0.0.1:8099/deploy"],
|
||||
] as const;
|
||||
|
||||
export function runCyberPreflight(opts: CyberPreflightOptions): CyberPreflightReceipt {
|
||||
|
|
@ -45,7 +45,10 @@ export function runCyberPreflight(opts: CyberPreflightOptions): CyberPreflightRe
|
|||
checks.push(runCheck("docker_inventory", "ssh", ["-p", String(port), `root@${opts.host}`, "docker ps --format '{{.Names}} {{.Status}} {{.Ports}}'"], runner));
|
||||
|
||||
for (const [name, url] of REQUIRED_ENDPOINTS) {
|
||||
checks.push(runCheck(name, "ssh", ["-p", String(port), `root@${opts.host}`, `curl -fsS -m 5 ${url}`], runner));
|
||||
const probe = name === "deploy_webhook_health"
|
||||
? `test "$(curl -sS -m 5 -o /dev/null -w '%{http_code}' -X POST ${url})" = "401"`
|
||||
: `curl -fsS -m 5 ${url}`;
|
||||
checks.push(runCheck(name, "ssh", ["-p", String(port), `root@${opts.host}`, probe], runner));
|
||||
}
|
||||
|
||||
const requiredOk = checks.filter((c) => c.name !== "docker_inventory").every((c) => c.ok);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function writeInputs(root: string, checks: Array<{ name: string; ok: boolean; st
|
|||
}, null, 2));
|
||||
fs.writeFileSync(capabilities, `services:
|
||||
deploy-webhook:
|
||||
url: http://127.0.0.1:8098/health
|
||||
url: http://127.0.0.1:8099/deploy
|
||||
provides: [deploy]
|
||||
required: true
|
||||
git-proxy:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { formatCapabilityReport, parseFactoryCapabilities, probeFactoryCapabilit
|
|||
|
||||
const yaml = `services:
|
||||
deploy-webhook:
|
||||
url: http://77.42.112.29:8098/health
|
||||
url: http://77.42.112.29:8099/deploy
|
||||
provides: [deploy]
|
||||
required: true
|
||||
git-proxy:
|
||||
|
|
@ -21,7 +21,7 @@ describe("factory capabilities", () => {
|
|||
|
||||
it("fails required capabilities that return 404", async () => {
|
||||
const services = parseFactoryCapabilities(yaml);
|
||||
const report = await probeFactoryCapabilities(services, async (url) => ({ ok: !url.includes("8099"), status: url.includes("8099") ? 404 : 200 }));
|
||||
const report = await probeFactoryCapabilities(services, async (url) => ({ ok: url.includes("/deploy"), status: url.includes("/deploy") ? 401 : 404 }));
|
||||
|
||||
expect(report.requiredOk).toBe(false);
|
||||
expect(report.available).toContain("deploy");
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export interface CapabilityReport {
|
|||
requiredOk: boolean;
|
||||
}
|
||||
|
||||
type FetchLike = (url: string, init?: { signal?: AbortSignal }) => Promise<{ ok: boolean; status: number }>;
|
||||
type FetchLike = (url: string, init?: { method?: string; signal?: AbortSignal }) => Promise<{ ok: boolean; status: number }>;
|
||||
|
||||
export function parseFactoryCapabilities(text: string): FactoryCapabilityService[] {
|
||||
// ponytail: tiny YAML subset for this checked-in registry; add a parser only when the format grows.
|
||||
|
|
@ -86,8 +86,10 @@ async function probeCapability(svc: FactoryCapabilityService, fetcher: FetchLike
|
|||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetcher(svc.url, { signal: controller.signal });
|
||||
return { ...svc, ok: res.ok, status: res.status, error: res.ok ? undefined : `HTTP ${res.status}` };
|
||||
const res = await fetcher(svc.url, { method: svc.provides.includes("deploy") ? "POST" : "GET", signal: controller.signal });
|
||||
// ponytail: unauthenticated 401 proves the deploy route exists without requiring the deploy token.
|
||||
const ok = res.ok || (svc.provides.includes("deploy") && res.status === 401);
|
||||
return { ...svc, ok, status: res.status, error: ok ? undefined : `HTTP ${res.status}` };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.name === "AbortError" ? "timed out" : error instanceof Error ? error.message : String(error);
|
||||
return { ...svc, ok: false, error: message };
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ describe("factory check", () => {
|
|||
|
||||
it("accepts factory-status 200", async () => {
|
||||
const result = await checkFactory(async (url) =>
|
||||
url.includes(":8099/")
|
||||
url.includes("/factory-status")
|
||||
? ok({ status: "ok", containers: 25 })
|
||||
: ok({ status: "ok", service: "deploy-webhook" }),
|
||||
: fail(401),
|
||||
);
|
||||
|
||||
expect(result.factoryAvailable).toBe(true);
|
||||
|
|
@ -29,7 +29,7 @@ describe("factory check", () => {
|
|||
|
||||
it("degrades factory-status 404 to unknown while preserving deploy health", async () => {
|
||||
const result = await checkFactory(async (url) =>
|
||||
url.includes(":8099/") ? fail(404) : ok({ status: "ok", service: "deploy-webhook" }),
|
||||
url.includes("/factory-status") ? fail(404) : fail(401),
|
||||
);
|
||||
|
||||
expect(result.factoryAvailable).toBe(false);
|
||||
|
|
@ -42,7 +42,7 @@ describe("factory check", () => {
|
|||
|
||||
it("degrades malformed factory-status JSON to unknown", async () => {
|
||||
const result = await checkFactory(async (url) =>
|
||||
url.includes(":8099/") ? ok(null) : ok({ status: "ok", service: "deploy-webhook" }),
|
||||
url.includes("/factory-status") ? ok(null) : fail(401),
|
||||
);
|
||||
|
||||
expect(result.factoryAvailable).toBe(false);
|
||||
|
|
@ -52,7 +52,7 @@ describe("factory check", () => {
|
|||
|
||||
it("degrades factory-status timeout without using stale memory", async () => {
|
||||
const result = await checkFactory(async (url, init) => {
|
||||
if (!url.includes(":8099/")) return ok({ status: "ok", service: "deploy-webhook" });
|
||||
if (!url.includes("/factory-status")) return fail(401);
|
||||
await new Promise((_resolve, reject) => init?.signal?.addEventListener("abort", () => reject(Object.assign(new Error("aborted"), { name: "AbortError" }))));
|
||||
return ok({ status: "stale-memory-should-not-appear" });
|
||||
}, 1);
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ export interface FactoryCheckResult {
|
|||
}
|
||||
|
||||
const FACTORY_STATUS_URL = "http://77.42.112.29:8099/factory-status";
|
||||
const DEPLOY_HEALTH_URL = "http://77.42.112.29:8098/health";
|
||||
const DEPLOY_HEALTH_URL = "http://77.42.112.29:8099/deploy";
|
||||
|
||||
type FetchLike = (url: string, init?: { signal?: AbortSignal }) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;
|
||||
type FetchLike = (url: string, init?: { method?: string; signal?: AbortSignal }) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
|
|
@ -53,7 +53,7 @@ export async function checkFactory(fetcher: FetchLike = fetch, timeoutMs = 5_000
|
|||
// ponytail: live HTTP only; no memory fallback, because stale factory state must not open the deploy gate.
|
||||
const [factoryResult, deployResult] = await Promise.allSettled([
|
||||
getJson(fetcher, FACTORY_STATUS_URL, timeoutMs),
|
||||
getJson(fetcher, DEPLOY_HEALTH_URL, timeoutMs),
|
||||
checkDeployRoute(fetcher, timeoutMs),
|
||||
]);
|
||||
const errors: string[] = [];
|
||||
if (factoryResult.status === "rejected") errors.push(factoryResult.reason instanceof Error ? factoryResult.reason.message : String(factoryResult.reason));
|
||||
|
|
@ -68,6 +68,22 @@ export async function checkFactory(fetcher: FetchLike = fetch, timeoutMs = 5_000
|
|||
};
|
||||
}
|
||||
|
||||
async function checkDeployRoute(fetcher: FetchLike, timeoutMs: number): Promise<DeployHealth> {
|
||||
// ponytail: unauthenticated 401 proves the deploy route exists without leaking or requiring the deploy token.
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetcher(DEPLOY_HEALTH_URL, { method: "POST", signal: controller.signal });
|
||||
if (res.status !== 401 && !res.ok) throw new Error(`${DEPLOY_HEALTH_URL} returned ${res.status}`);
|
||||
return { status: "ok", service: "deploy-webhook" };
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") throw new Error(`${DEPLOY_HEALTH_URL} timed out`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export function factoryGateReady(result: FactoryCheckResult): boolean {
|
||||
return result.factoryAvailable && result.deployAvailable && result.factory.status === "ok" && result.deploy.status === "ok";
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue