docker socket

This commit is contained in:
Rajesh V 2025-05-29 22:34:05 +05:30
parent 23b5dcfbed
commit 948eb7f6d0
21 changed files with 1808 additions and 128 deletions

View file

@ -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", {

View file

@ -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,

View file

@ -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
};

View 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);
}

View 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`);
}
};

View file

@ -1,3 +1,4 @@
export * from "./createNewt";
export * from "./getToken";
export * from "./handleRegisterMessage";
export * from "./handleRegisterMessage";
export * from "./handleSocketMessages";

View file

@ -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";

View 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")
);
}
}

View file

@ -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)