175 lines
6.2 KiB
JavaScript
175 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createServer } from "node:http";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
import { jsonResponse, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
|
|
|
|
const SERVICE_ID = "hwlab-device-pod";
|
|
const CONTRACT_VERSION = "device-pod-executor-v1";
|
|
const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq";
|
|
const port = parsePort(process.env.HWLAB_DEVICE_POD_PORT, parsePort(process.env.PORT, 7601));
|
|
const devicePodId = process.env.HWLAB_DEVICE_POD_ID || DEFAULT_DEVICE_POD_ID;
|
|
const environment = process.env.HWLAB_ENVIRONMENT || process.env.HWLAB_GITOPS_PROFILE || "dev";
|
|
|
|
listen(createServer(async (request, response) => {
|
|
try {
|
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
|
|
jsonResponse(response, 200, healthPayload());
|
|
return;
|
|
}
|
|
if (url.pathname === "/health" || url.pathname === "/health/live") {
|
|
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN", path: url.pathname });
|
|
return;
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/v1/device-pods") {
|
|
jsonResponse(response, 200, listPayload());
|
|
return;
|
|
}
|
|
const route = parseDevicePodPath(url.pathname);
|
|
if (!route) {
|
|
jsonResponse(response, 404, { error: "not_found", path: url.pathname });
|
|
return;
|
|
}
|
|
if (request.method === "GET" && route.subpath === "status") {
|
|
jsonResponse(response, 200, statusPayload(route.devicePodId));
|
|
return;
|
|
}
|
|
if (request.method === "GET" && route.subpath === "events") {
|
|
jsonResponse(response, 200, eventsPayload(route.devicePodId, url.searchParams));
|
|
return;
|
|
}
|
|
if (request.method === "POST" && route.subpath === "jobs") {
|
|
await handleInternalJob(request, response, route.devicePodId);
|
|
return;
|
|
}
|
|
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN", path: url.pathname });
|
|
} catch (error) {
|
|
jsonResponse(response, 500, { error: "internal_error", message: error?.message ?? String(error), valuesRedacted: true });
|
|
}
|
|
}), port);
|
|
|
|
function healthPayload() {
|
|
return {
|
|
serviceId: SERVICE_ID,
|
|
environment,
|
|
status: "live",
|
|
ready: true,
|
|
contractVersion: CONTRACT_VERSION,
|
|
devicePodId,
|
|
observedAt: new Date().toISOString(),
|
|
role: "device-pod-internal-executor",
|
|
authority: "hwlab-cloud-api",
|
|
acceptsUserAuthority: false,
|
|
fake: false,
|
|
source: sourcePayload(),
|
|
note: "Profile, grant, lease, and user-facing job authority live in hwlab-cloud-api; this service only exposes the internal executor boundary."
|
|
};
|
|
}
|
|
|
|
function listPayload() {
|
|
return {
|
|
serviceId: SERVICE_ID,
|
|
contractVersion: CONTRACT_VERSION,
|
|
status: "ok",
|
|
source: sourcePayload(),
|
|
selectedDevicePodId: devicePodId,
|
|
devicePods: [{
|
|
devicePodId,
|
|
status: "executor-ready",
|
|
authority: "hwlab-cloud-api",
|
|
fake: false,
|
|
routes: {
|
|
cloudAuthority: "/v1/device-pods",
|
|
internalJob: `/v1/device-pods/${encodeURIComponent(devicePodId)}/jobs`
|
|
}
|
|
}]
|
|
};
|
|
}
|
|
|
|
function statusPayload(id) {
|
|
return {
|
|
serviceId: SERVICE_ID,
|
|
contractVersion: CONTRACT_VERSION,
|
|
status: "blocked",
|
|
devicePodId: id,
|
|
observedAt: new Date().toISOString(),
|
|
source: sourcePayload(),
|
|
summary: {
|
|
devicePodId: id,
|
|
status: "blocked",
|
|
blocker: gatewayAdapterBlocker("device-host-cli dispatch is owned by hwlab-cloud-api gateway routing")
|
|
}
|
|
};
|
|
}
|
|
|
|
function eventsPayload(id, searchParams) {
|
|
const limit = Math.min(Math.max(Number.parseInt(searchParams.get("limit") ?? "80", 10) || 80, 1), 1000);
|
|
const event = {
|
|
eventId: `evt_${id}_executor_boundary`,
|
|
devicePodId: id,
|
|
ts: new Date().toISOString(),
|
|
level: "warn",
|
|
scope: "executor",
|
|
status: "blocked",
|
|
summary: "No standalone fake device events are emitted by hwlab-device-pod; use hwlab-cloud-api job authority.",
|
|
blocker: gatewayAdapterBlocker("standalone device-pod executor has no gateway session")
|
|
};
|
|
return {
|
|
serviceId: SERVICE_ID,
|
|
contractVersion: CONTRACT_VERSION,
|
|
status: "ok",
|
|
devicePodId: id,
|
|
source: sourcePayload(),
|
|
events: limit > 0 ? [event] : [],
|
|
truncation: { limit, returned: limit > 0 ? 1 : 0, truncated: false }
|
|
};
|
|
}
|
|
|
|
async function handleInternalJob(request, response, id) {
|
|
if (!isInternalCaller(request)) {
|
|
jsonResponse(response, 403, {
|
|
accepted: false,
|
|
status: "blocked",
|
|
error: {
|
|
code: "cloud_api_authority_required",
|
|
message: "Device Pod jobs must be authorized by hwlab-cloud-api before reaching the internal executor."
|
|
},
|
|
source: sourcePayload()
|
|
});
|
|
return;
|
|
}
|
|
const body = await readJson(request);
|
|
const traceId = typeof body.traceId === "string" && body.traceId.startsWith("trc_") ? body.traceId : `trc_devicepod_${randomUUID()}`;
|
|
jsonResponse(response, 409, {
|
|
accepted: false,
|
|
status: "blocked",
|
|
contractVersion: CONTRACT_VERSION,
|
|
devicePodId: id,
|
|
traceId,
|
|
operationId: typeof body.operationId === "string" ? body.operationId : `op_devicepod_${randomUUID()}`,
|
|
blocker: gatewayAdapterBlocker("gateway/device-host-cli adapter is not connected to this standalone executor service"),
|
|
source: sourcePayload()
|
|
});
|
|
}
|
|
|
|
function parseDevicePodPath(pathname) {
|
|
const prefix = "/v1/device-pods/";
|
|
if (!pathname.startsWith(prefix)) return null;
|
|
const [id, ...rest] = pathname.slice(prefix.length).split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
|
return id ? { devicePodId: id, subpath: rest.join("/") } : null;
|
|
}
|
|
|
|
function isInternalCaller(request) {
|
|
const header = request.headers["x-hwlab-internal-service"];
|
|
return String(Array.isArray(header) ? header[0] : header ?? "").trim() === "hwlab-cloud-api";
|
|
}
|
|
|
|
function sourcePayload() {
|
|
return { kind: "INTERNAL_EXECUTOR", serviceId: SERVICE_ID, authority: "hwlab-cloud-api", fake: false, devLiveEvidence: false };
|
|
}
|
|
|
|
function gatewayAdapterBlocker(summary) {
|
|
return { code: "gateway_dispatch_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod executor boundary is present, but gateway/device-host-cli dispatch is not connected here." };
|
|
}
|