fix: align factory probes with git-proxy routes
This commit is contained in:
parent
9df2929446
commit
2fadb8a428
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
deploy-webhook:
|
deploy-webhook:
|
||||||
url: http://77.42.112.29:8098/health
|
url: http://77.42.112.29:8099/deploy
|
||||||
provides: [deploy]
|
provides: [deploy]
|
||||||
required: true
|
required: true
|
||||||
git-proxy:
|
git-proxy:
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export interface CyberPreflightOptions {
|
||||||
const REQUIRED_ENDPOINTS = [
|
const REQUIRED_ENDPOINTS = [
|
||||||
["git_proxy_health", "http://127.0.0.1:8099/health"],
|
["git_proxy_health", "http://127.0.0.1:8099/health"],
|
||||||
["git_proxy_factory_status", "http://127.0.0.1:8099/factory-status"],
|
["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;
|
] as const;
|
||||||
|
|
||||||
export function runCyberPreflight(opts: CyberPreflightOptions): CyberPreflightReceipt {
|
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));
|
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) {
|
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);
|
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));
|
}, null, 2));
|
||||||
fs.writeFileSync(capabilities, `services:
|
fs.writeFileSync(capabilities, `services:
|
||||||
deploy-webhook:
|
deploy-webhook:
|
||||||
url: http://127.0.0.1:8098/health
|
url: http://127.0.0.1:8099/deploy
|
||||||
provides: [deploy]
|
provides: [deploy]
|
||||||
required: true
|
required: true
|
||||||
git-proxy:
|
git-proxy:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { formatCapabilityReport, parseFactoryCapabilities, probeFactoryCapabilit
|
||||||
|
|
||||||
const yaml = `services:
|
const yaml = `services:
|
||||||
deploy-webhook:
|
deploy-webhook:
|
||||||
url: http://77.42.112.29:8098/health
|
url: http://77.42.112.29:8099/deploy
|
||||||
provides: [deploy]
|
provides: [deploy]
|
||||||
required: true
|
required: true
|
||||||
git-proxy:
|
git-proxy:
|
||||||
|
|
@ -21,7 +21,7 @@ describe("factory capabilities", () => {
|
||||||
|
|
||||||
it("fails required capabilities that return 404", async () => {
|
it("fails required capabilities that return 404", async () => {
|
||||||
const services = parseFactoryCapabilities(yaml);
|
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.requiredOk).toBe(false);
|
||||||
expect(report.available).toContain("deploy");
|
expect(report.available).toContain("deploy");
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export interface CapabilityReport {
|
||||||
requiredOk: boolean;
|
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[] {
|
export function parseFactoryCapabilities(text: string): FactoryCapabilityService[] {
|
||||||
// ponytail: tiny YAML subset for this checked-in registry; add a parser only when the format grows.
|
// 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 controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const res = await fetcher(svc.url, { signal: controller.signal });
|
const res = await fetcher(svc.url, { method: svc.provides.includes("deploy") ? "POST" : "GET", signal: controller.signal });
|
||||||
return { ...svc, ok: res.ok, status: res.status, error: res.ok ? undefined : `HTTP ${res.status}` };
|
// 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) {
|
} catch (error) {
|
||||||
const message = error instanceof Error && error.name === "AbortError" ? "timed out" : error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error && error.name === "AbortError" ? "timed out" : error instanceof Error ? error.message : String(error);
|
||||||
return { ...svc, ok: false, error: message };
|
return { ...svc, ok: false, error: message };
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ describe("factory check", () => {
|
||||||
|
|
||||||
it("accepts factory-status 200", async () => {
|
it("accepts factory-status 200", async () => {
|
||||||
const result = await checkFactory(async (url) =>
|
const result = await checkFactory(async (url) =>
|
||||||
url.includes(":8099/")
|
url.includes("/factory-status")
|
||||||
? ok({ status: "ok", containers: 25 })
|
? ok({ status: "ok", containers: 25 })
|
||||||
: ok({ status: "ok", service: "deploy-webhook" }),
|
: fail(401),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.factoryAvailable).toBe(true);
|
expect(result.factoryAvailable).toBe(true);
|
||||||
|
|
@ -29,7 +29,7 @@ describe("factory check", () => {
|
||||||
|
|
||||||
it("degrades factory-status 404 to unknown while preserving deploy health", async () => {
|
it("degrades factory-status 404 to unknown while preserving deploy health", async () => {
|
||||||
const result = await checkFactory(async (url) =>
|
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);
|
expect(result.factoryAvailable).toBe(false);
|
||||||
|
|
@ -42,7 +42,7 @@ describe("factory check", () => {
|
||||||
|
|
||||||
it("degrades malformed factory-status JSON to unknown", async () => {
|
it("degrades malformed factory-status JSON to unknown", async () => {
|
||||||
const result = await checkFactory(async (url) =>
|
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);
|
expect(result.factoryAvailable).toBe(false);
|
||||||
|
|
@ -52,7 +52,7 @@ describe("factory check", () => {
|
||||||
|
|
||||||
it("degrades factory-status timeout without using stale memory", async () => {
|
it("degrades factory-status timeout without using stale memory", async () => {
|
||||||
const result = await checkFactory(async (url, init) => {
|
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" }))));
|
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" });
|
return ok({ status: "stale-memory-should-not-appear" });
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@ export interface FactoryCheckResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FACTORY_STATUS_URL = "http://77.42.112.29:8099/factory-status";
|
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> {
|
function asRecord(value: unknown): Record<string, unknown> {
|
||||||
return value && typeof value === "object" && !Array.isArray(value) ? value as 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.
|
// ponytail: live HTTP only; no memory fallback, because stale factory state must not open the deploy gate.
|
||||||
const [factoryResult, deployResult] = await Promise.allSettled([
|
const [factoryResult, deployResult] = await Promise.allSettled([
|
||||||
getJson(fetcher, FACTORY_STATUS_URL, timeoutMs),
|
getJson(fetcher, FACTORY_STATUS_URL, timeoutMs),
|
||||||
getJson(fetcher, DEPLOY_HEALTH_URL, timeoutMs),
|
checkDeployRoute(fetcher, timeoutMs),
|
||||||
]);
|
]);
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
if (factoryResult.status === "rejected") errors.push(factoryResult.reason instanceof Error ? factoryResult.reason.message : String(factoryResult.reason));
|
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 {
|
export function factoryGateReady(result: FactoryCheckResult): boolean {
|
||||||
return result.factoryAvailable && result.deployAvailable && result.factory.status === "ok" && result.deploy.status === "ok";
|
return result.factoryAvailable && result.deployAvailable && result.factory.status === "ok" && result.deploy.status === "ok";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue