pangolin/server/routers/site/socketIntegration.ts
2025-06-04 15:24:15 -04:00

283 lines
7.3 KiB
TypeScript

import { db } from "@server/db";
import { newts, sites } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import stoi from "@server/lib/stoi";
import { sendToClient } from "../ws";
import {
fetchContainers,
dockerSocketCache,
dockerSocket
} from "../newt/dockerSocket";
export interface ContainerNetwork {
networkId: string;
endpointId: string;
gateway?: string;
ipAddress?: string;
ipPrefixLen?: number;
macAddress?: string;
aliases?: string[];
dnsNames?: string[];
}
export interface ContainerPort {
privatePort: number;
publicPort?: number;
type: "tcp" | "udp";
ip?: string;
}
export interface Container {
id: string;
name: string;
image: string;
state: "running" | "exited" | "paused" | "created";
status: string;
ports?: ContainerPort[];
labels: Record<string, string>;
created: number;
networks: Record<string, ContainerNetwork>;
}
const siteIdParamsSchema = z
.object({
siteId: z.string().transform(stoi).pipe(z.number().int().positive())
})
.strict();
const DockerStatusSchema = z
.object({
isAvailable: z.boolean(),
socketPath: z.string().optional()
})
.strict();
function validateSiteIdParams(params: any) {
const parsedParams = siteIdParamsSchema.safeParse(params);
if (!parsedParams.success) {
throw createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
);
}
return parsedParams.data;
}
async function getSiteAndValidateNewt(siteId: number) {
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site) {
throw createHttpError(HttpCode.NOT_FOUND, "Site not found");
}
if (site.type !== "newt") {
throw createHttpError(
HttpCode.BAD_REQUEST,
"This endpoint is only for Newt sites"
);
}
return site;
}
async function getNewtBySiteId(siteId: number) {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, siteId))
.limit(1);
if (!newt) {
throw createHttpError(HttpCode.NOT_FOUND, "Newt not found for site");
}
return newt;
}
async function getSiteAndNewt(siteId: number) {
const site = await getSiteAndValidateNewt(siteId);
const newt = await getNewtBySiteId(siteId);
return { site, newt };
}
function asyncHandler(
operation: (siteId: number) => Promise<any>,
successMessage: string
) {
return async (
req: Request,
res: Response,
next: NextFunction
): Promise<any> => {
try {
const { siteId } = validateSiteIdParams(req.params);
const result = await operation(siteId);
return response(res, {
data: result,
success: true,
error: false,
message: successMessage,
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred"
)
);
}
};
}
// Core business logic functions
async function triggerFetch(siteId: number) {
const { newt } = await getSiteAndNewt(siteId);
logger.info(
`Triggering fetch containers for site ${siteId} with Newt ${newt.newtId}`
);
fetchContainers(newt.newtId);
// clear the cache for this Newt ID so that the site has to keep asking for the containers
// this is to ensure that the site always gets the latest data
dockerSocketCache.del(`${newt.newtId}:dockerContainers`);
return { siteId, newtId: newt.newtId };
}
async function queryContainers(siteId: number) {
const { newt } = await getSiteAndNewt(siteId);
const result = dockerSocketCache.get(
`${newt.newtId}:dockerContainers`
) as Container[];
if (!result) {
throw createHttpError(
HttpCode.TOO_EARLY,
"Nothing found yet. Perhaps the fetch is still in progress? Wait a bit and try again."
);
}
return result;
}
async function isDockerAvailable(siteId: number): Promise<boolean> {
const { newt } = await getSiteAndNewt(siteId);
const key = `${newt.newtId}:isAvailable`;
const isAvailable = dockerSocketCache.get(key);
return !!isAvailable;
}
async function getDockerStatus(
siteId: number
): Promise<z.infer<typeof DockerStatusSchema>> {
const { newt } = await getSiteAndNewt(siteId);
const keys = ["isAvailable", "socketPath"];
const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`);
const result = {
isAvailable: dockerSocketCache.get(mappedKeys[0]) as boolean,
socketPath: dockerSocketCache.get(mappedKeys[1]) as string | undefined
};
return result;
}
async function checkSocket(
siteId: number
): Promise<{ siteId: number; newtId: string }> {
const { newt } = await getSiteAndNewt(siteId);
logger.info(
`Checking Docker socket for site ${siteId} with Newt ${newt.newtId}`
);
// Trigger the Docker socket check
dockerSocket(newt.newtId);
return { siteId, newtId: newt.newtId };
}
// Export types
export type GetDockerStatusResponse = NonNullable<
Awaited<ReturnType<typeof getDockerStatus>>
>;
export type ListContainersResponse = Awaited<
ReturnType<typeof queryContainers>
>;
export type TriggerFetchResponse = Awaited<ReturnType<typeof triggerFetch>>;
// Route handlers
export const triggerFetchContainers = asyncHandler(
triggerFetch,
"Fetch containers triggered successfully"
);
export const listContainers = asyncHandler(
queryContainers,
"Containers retrieved successfully"
);
export const dockerOnline = asyncHandler(async (siteId: number) => {
const isAvailable = await isDockerAvailable(siteId);
return { isAvailable };
}, "Docker availability checked successfully");
export const dockerStatus = asyncHandler(
getDockerStatus,
"Docker status retrieved successfully"
);
export async function checkDockerSocket(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const { siteId } = validateSiteIdParams(req.params);
const result = await checkSocket(siteId);
// Notify the Newt client about the Docker socket check
sendToClient(result.newtId, {
type: "newt/socket/check",
data: {}
});
return response(res, {
data: result,
success: true,
error: false,
message: "Docker socket checked successfully",
status: HttpCode.OK
});
} catch (error) {
if (createHttpError.isHttpError(error)) {
return next(error);
}
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}