Archived
feat: apply agent profile settings atomically
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { FastifyInstance, FastifyReply } from "fastify";
|
||||
import type { WebSocket } from "ws";
|
||||
import type { PiWebAgentConfig } from "../../shared/apiTypes.js";
|
||||
import { FEDERATED_HTTP_ROUTES, FEDERATED_WEBSOCKET_ROUTES, type FederatedHttpRouteSpec } from "../../shared/federatedRoutes.js";
|
||||
import { mergeSelectedMachineConfig, parsePiWebConfigResponseBody, parseSelectedMachineConfigRequest, selectedMachineConfigResponse } from "../configRoutes.js";
|
||||
import { bridgeSockets } from "../webSocketBridge.js";
|
||||
@@ -75,7 +76,8 @@ async function proxySelectedMachineConfigRequest(client: MachineClient, machineI
|
||||
|
||||
const current = parsePiWebConfigResponseBody(currentResponse.body, "Remote machine config response");
|
||||
const merged = mergeSelectedMachineConfig(current.config, patch);
|
||||
return sendSelectedMachineConfigResponse(reply, await client.requestJson("PUT", remotePath, { config: merged }), machineId);
|
||||
const updateResponse = await client.requestJson("PUT", remotePath, { config: merged });
|
||||
return sendSelectedMachineConfigResponse(reply, updateResponse, machineId, patch.agent);
|
||||
}
|
||||
|
||||
return reply.code(405).send({ error: "Method not allowed" });
|
||||
@@ -85,11 +87,23 @@ function configPayload(body: unknown): unknown {
|
||||
return isRecord(body) ? body["config"] : undefined;
|
||||
}
|
||||
|
||||
function sendSelectedMachineConfigResponse(reply: FastifyReply, upstream: MachineJsonResponse, machineId: string): FastifyReply {
|
||||
function sendSelectedMachineConfigResponse(reply: FastifyReply, upstream: MachineJsonResponse, machineId: string, expectedAgentProfile?: PiWebAgentConfig): FastifyReply {
|
||||
if (!isSuccessfulStatus(upstream.statusCode)) return sendUpstreamJsonResponse(reply, upstream, machineId);
|
||||
const response = parsePiWebConfigResponseBody(upstream.body, "Remote machine config response");
|
||||
if (expectedAgentProfile !== undefined && !sameAgentProfile(response.config.agent, expectedAgentProfile)) {
|
||||
return reply.code(409).send({
|
||||
error: "Remote machine did not persist the requested agent profile",
|
||||
machineId,
|
||||
detail: "Update and restart PI WEB on the remote machine before changing its agent profile.",
|
||||
});
|
||||
}
|
||||
reply.code(upstream.statusCode);
|
||||
applySafeHeaders(reply, upstream.headers);
|
||||
return reply.send(selectedMachineConfigResponse(parsePiWebConfigResponseBody(upstream.body, "Remote machine config response")));
|
||||
return reply.send(selectedMachineConfigResponse(response));
|
||||
}
|
||||
|
||||
function sameAgentProfile(actual: PiWebAgentConfig | undefined, expected: PiWebAgentConfig): boolean {
|
||||
return actual !== undefined && actual.command === expected.command && actual.dir === expected.dir;
|
||||
}
|
||||
|
||||
function sendUpstreamJsonResponse(reply: FastifyReply, upstream: MachineJsonResponse, machineId: string): FastifyReply {
|
||||
|
||||
@@ -18,8 +18,8 @@ export function registerMachineRoutes(app: FastifyInstance, machines = new Machi
|
||||
return health;
|
||||
});
|
||||
|
||||
app.get<{ Params: { machineId: string } }>("/api/machines/:machineId/runtime", async (request, reply) => {
|
||||
const runtime = await machines.runtime(request.params.machineId);
|
||||
app.get<{ Params: { machineId: string }; Querystring: { refresh?: string } }>("/api/machines/:machineId/runtime", async (request, reply) => {
|
||||
const runtime = await machines.runtime(request.params.machineId, request.query.refresh === "1");
|
||||
if (runtime === undefined) return reply.code(404).send({ error: "Machine not found" });
|
||||
return runtime;
|
||||
});
|
||||
|
||||
@@ -171,6 +171,7 @@ describe("MachineService", () => {
|
||||
|
||||
const first = await remoteService.runtime(machine.id);
|
||||
const second = await remoteService.runtime(machine.id);
|
||||
const forced = await remoteService.runtime(machine.id, true);
|
||||
|
||||
expect(first).toEqual({
|
||||
machineId: machine.id,
|
||||
@@ -182,7 +183,8 @@ describe("MachineService", () => {
|
||||
capabilities: body.capabilities,
|
||||
});
|
||||
expect(second).toEqual(first);
|
||||
expect(requestJson).toHaveBeenCalledTimes(1);
|
||||
expect(forced).toEqual(first);
|
||||
expect(requestJson).toHaveBeenCalledTimes(2);
|
||||
expect(requestJson).toHaveBeenCalledWith("GET", "/api/pi-web/runtime", undefined, { timeoutMs: 3000 });
|
||||
expect(factoryMachines).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -192,6 +194,7 @@ describe("MachineService", () => {
|
||||
token: "secret",
|
||||
headers: { "X-Pi-Web-Test": "yes" },
|
||||
}),
|
||||
expect.objectContaining({ id: machine.id }),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -93,10 +93,10 @@ export class MachineService {
|
||||
return health;
|
||||
}
|
||||
|
||||
async runtime(id: string): Promise<MachineRuntime | undefined> {
|
||||
async runtime(id: string, refresh = false): Promise<MachineRuntime | undefined> {
|
||||
const cached = this.runtimeCache.get(id);
|
||||
const now = this.now().getTime();
|
||||
if (cached !== undefined && cached.expiresAt > now) return cached.runtime;
|
||||
if (!refresh && cached !== undefined && cached.expiresAt > now) return cached.runtime;
|
||||
|
||||
const runtime = id === "local" ? await this.localRuntime() : await this.remoteRuntime(id);
|
||||
if (runtime === undefined) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user