mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-03 17:44:44 +02:00
docker socket
This commit is contained in:
parent
23b5dcfbed
commit
948eb7f6d0
21 changed files with 1808 additions and 128 deletions
|
@ -41,7 +41,10 @@ export const sites = sqliteTable("sites", {
|
|||
megabytesOut: integer("bytesOut"),
|
||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||
type: text("type").notNull(), // "newt" or "wireguard"
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false)
|
||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false)
|
||||
});
|
||||
|
||||
export const resources = sqliteTable("resources", {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
getUserOrgs,
|
||||
verifyUserIsServerAdmin,
|
||||
verifyIsLoggedInUser,
|
||||
verifyApiKeyAccess,
|
||||
verifyApiKeyAccess
|
||||
} from "@server/middlewares";
|
||||
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
|
@ -124,6 +124,37 @@ authenticated.delete(
|
|||
site.deleteSite
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/status",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.dockerStatus
|
||||
);
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/online",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.dockerOnline
|
||||
);
|
||||
authenticated.post(
|
||||
"/site/:siteId/docker/check",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.checkDockerSocket
|
||||
);
|
||||
authenticated.post(
|
||||
"/site/:siteId/docker/trigger",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.triggerFetchContainers
|
||||
);
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/containers",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.listContainers
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
verifyOrgAccess,
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { handleRegisterMessage } from "./newt";
|
||||
import {
|
||||
handleRegisterMessage,
|
||||
handleDockerStatusMessage,
|
||||
handleDockerContainersMessage
|
||||
} from "./newt";
|
||||
import { MessageHandler } from "./ws";
|
||||
|
||||
export const messageHandlers: Record<string, MessageHandler> = {
|
||||
"newt/wg/register": handleRegisterMessage,
|
||||
};
|
||||
"newt/socket/status": handleDockerStatusMessage,
|
||||
"newt/socket/containers": handleDockerContainersMessage
|
||||
};
|
||||
|
|
22
server/routers/newt/dockerSocket.ts
Normal file
22
server/routers/newt/dockerSocket.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import NodeCache from "node-cache";
|
||||
import { sendToClient } from "../ws";
|
||||
|
||||
export const dockerSocketCache = new NodeCache({
|
||||
stdTTL: 3600 // seconds
|
||||
});
|
||||
|
||||
export function fetchContainers(newtId: string) {
|
||||
const payload = {
|
||||
type: `newt/socket/fetch`,
|
||||
data: {}
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
|
||||
export function dockerSocket(newtId: string) {
|
||||
const payload = {
|
||||
type: `newt/socket/check`,
|
||||
data: {}
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
57
server/routers/newt/handleSocketMessages.ts
Normal file
57
server/routers/newt/handleSocketMessages.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { MessageHandler } from "../ws";
|
||||
import logger from "@server/logger";
|
||||
import { dockerSocketCache } from "./dockerSocket";
|
||||
|
||||
export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||
const { message, newt } = context;
|
||||
|
||||
logger.info("Handling Docker socket check response");
|
||||
|
||||
if (!newt) {
|
||||
logger.warn("Newt not found");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Newt ID: ${newt.newtId}, Site ID: ${newt.siteId}`);
|
||||
const { available, socketPath } = message.data;
|
||||
|
||||
logger.info(
|
||||
`Docker socket availability for Newt ${newt.newtId}: available=${available}, socketPath=${socketPath}`
|
||||
);
|
||||
|
||||
if (available) {
|
||||
logger.info(`Newt ${newt.newtId} has Docker socket access`);
|
||||
dockerSocketCache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
||||
dockerSocketCache.set(`${newt.newtId}:isAvailable`, available, 0);
|
||||
} else {
|
||||
logger.warn(`Newt ${newt.newtId} does not have Docker socket access`);
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const handleDockerContainersMessage: MessageHandler = async (
|
||||
context
|
||||
) => {
|
||||
const { message, newt } = context;
|
||||
|
||||
logger.info("Handling Docker containers response");
|
||||
|
||||
if (!newt) {
|
||||
logger.warn("Newt not found");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Newt ID: ${newt.newtId}, Site ID: ${newt.siteId}`);
|
||||
const { containers } = message.data;
|
||||
|
||||
logger.info(
|
||||
`Docker containers for Newt ${newt.newtId}: ${containers ? containers.length : 0}`
|
||||
);
|
||||
|
||||
if (containers && containers.length > 0) {
|
||||
dockerSocketCache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
||||
} else {
|
||||
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
||||
}
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./createNewt";
|
||||
export * from "./getToken";
|
||||
export * from "./handleRegisterMessage";
|
||||
export * from "./handleRegisterMessage";
|
||||
export * from "./handleSocketMessages";
|
|
@ -3,5 +3,6 @@ export * from "./createSite";
|
|||
export * from "./deleteSite";
|
||||
export * from "./updateSite";
|
||||
export * from "./listSites";
|
||||
export * from "./listSiteRoles"
|
||||
export * from "./pickSiteDefaults";
|
||||
export * from "./listSiteRoles";
|
||||
export * from "./pickSiteDefaults";
|
||||
export * from "./socketIntegration";
|
||||
|
|
278
server/routers/site/socketIntegration.ts
Normal file
278
server/routers/site/socketIntegration.ts
Normal file
|
@ -0,0 +1,278 @@
|
|||
import { db } from "@server/db";
|
||||
import { newts, sites } from "@server/db/schemas";
|
||||
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);
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ const updateSiteParamsSchema = z
|
|||
const updateSiteBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
// subdomain: z
|
||||
// .string()
|
||||
// .min(1)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue