mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: system optional capabilities
This commit is contained in:
@@ -3,11 +3,12 @@ await Bun.build({
|
||||
outdir: "./dist",
|
||||
target: "bun",
|
||||
env: "disable",
|
||||
// sourcemap: "linked",
|
||||
sourcemap: true,
|
||||
minify: {
|
||||
whitespace: true,
|
||||
identifiers: true,
|
||||
syntax: true,
|
||||
keepNames: true,
|
||||
},
|
||||
external: ["ssh2"],
|
||||
});
|
||||
|
||||
55
apps/server/src/core/capabilities.ts
Normal file
55
apps/server/src/core/capabilities.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import Docker from "dockerode";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export type SystemCapabilities = {
|
||||
docker: boolean;
|
||||
};
|
||||
|
||||
let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
|
||||
|
||||
/**
|
||||
* Returns the current system capabilities.
|
||||
* On first call, detects all capabilities and caches the promise.
|
||||
* Subsequent calls return the same cached promise, ensuring detection only happens once.
|
||||
*/
|
||||
export async function getCapabilities(): Promise<SystemCapabilities> {
|
||||
if (capabilitiesPromise === null) {
|
||||
// Start detection and cache the promise
|
||||
capabilitiesPromise = detectCapabilities();
|
||||
}
|
||||
|
||||
return capabilitiesPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects which optional capabilities are available in the current environment
|
||||
*/
|
||||
async function detectCapabilities(): Promise<SystemCapabilities> {
|
||||
return {
|
||||
docker: await detectDocker(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Docker is available by:
|
||||
* 1. Checking if /var/run/docker.sock exists and is accessible
|
||||
* 2. Attempting to ping the Docker daemon
|
||||
*/
|
||||
async function detectDocker(): Promise<boolean> {
|
||||
try {
|
||||
await fs.access("/var/run/docker.sock");
|
||||
|
||||
const docker = new Docker();
|
||||
await docker.ping();
|
||||
|
||||
logger.info("Docker capability: enabled");
|
||||
return true;
|
||||
} catch (_) {
|
||||
logger.warn(
|
||||
"Docker capability: disabled. " +
|
||||
"To enable: mount /var/run/docker.sock and /run/docker/plugins in docker-compose.yml",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/bun";
|
||||
import { logger as honoLogger } from "hono/logger";
|
||||
import { openAPIRouteHandler } from "hono-openapi";
|
||||
import { getCapabilities } from "./core/capabilities";
|
||||
import { runDbMigrations } from "./db/db";
|
||||
import { authController } from "./modules/auth/auth.controller";
|
||||
import { requireAuth } from "./modules/auth/auth.middleware";
|
||||
import { driverController } from "./modules/driver/driver.controller";
|
||||
import { startup } from "./modules/lifecycle/startup";
|
||||
import { repositoriesController } from "./modules/repositories/repositories.controller";
|
||||
import { systemController } from "./modules/system/system.controller";
|
||||
import { volumeController } from "./modules/volumes/volume.controller";
|
||||
import { backupScheduleController } from "./modules/backups/backups.controller";
|
||||
import { handleServiceError } from "./utils/errors";
|
||||
@@ -41,6 +43,7 @@ const app = new Hono()
|
||||
.route("/api/v1/volumes", volumeController.use(requireAuth))
|
||||
.route("/api/v1/repositories", repositoriesController.use(requireAuth))
|
||||
.route("/api/v1/backups", backupScheduleController.use(requireAuth))
|
||||
.route("/api/v1/system", systemController.use(requireAuth))
|
||||
.get("/assets/*", serveStatic({ root: "./assets/frontend" }))
|
||||
.get("/images/*", serveStatic({ root: "./assets/frontend" }))
|
||||
.get("*", serveStatic({ path: "./assets/frontend/index.html" }));
|
||||
@@ -60,15 +63,26 @@ app.onError((err, c) => {
|
||||
return c.json({ message }, status);
|
||||
});
|
||||
|
||||
const socketPath = "/run/docker/plugins/ironmount.sock";
|
||||
|
||||
await fs.mkdir("/run/docker/plugins", { recursive: true });
|
||||
runDbMigrations();
|
||||
|
||||
Bun.serve({
|
||||
unix: socketPath,
|
||||
fetch: driver.fetch,
|
||||
});
|
||||
const { docker } = await getCapabilities();
|
||||
|
||||
if (docker) {
|
||||
const socketPath = "/run/docker/plugins/ironmount.sock";
|
||||
|
||||
try {
|
||||
await fs.mkdir("/run/docker/plugins", { recursive: true });
|
||||
|
||||
Bun.serve({
|
||||
unix: socketPath,
|
||||
fetch: driver.fetch,
|
||||
});
|
||||
|
||||
logger.info(`Docker volume plugin server running at ${socketPath}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to start Docker volume plugin server: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: 4096,
|
||||
@@ -77,6 +91,6 @@ Bun.serve({
|
||||
|
||||
startup();
|
||||
|
||||
logger.info(`Server is running at http://localhost:4096 and unix socket at ${socketPath}`);
|
||||
logger.info(`Server is running at http://localhost:4096`);
|
||||
|
||||
export type AppType = typeof app;
|
||||
|
||||
9
apps/server/src/modules/system/system.controller.ts
Normal file
9
apps/server/src/modules/system/system.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Hono } from "hono";
|
||||
import { systemInfoDto, type SystemInfoDto } from "./system.dto";
|
||||
import { systemService } from "./system.service";
|
||||
|
||||
export const systemController = new Hono().get("/info", systemInfoDto, async (c) => {
|
||||
const info = await systemService.getSystemInfo();
|
||||
|
||||
return c.json<SystemInfoDto>(info, 200);
|
||||
});
|
||||
28
apps/server/src/modules/system/system.dto.ts
Normal file
28
apps/server/src/modules/system/system.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type } from "arktype";
|
||||
import { describeRoute, resolver } from "hono-openapi";
|
||||
|
||||
export const capabilitiesSchema = type({
|
||||
docker: "boolean",
|
||||
});
|
||||
|
||||
export const systemInfoResponse = type({
|
||||
capabilities: capabilitiesSchema,
|
||||
});
|
||||
|
||||
export type SystemInfoDto = typeof systemInfoResponse.infer;
|
||||
|
||||
export const systemInfoDto = describeRoute({
|
||||
description: "Get system information including available capabilities",
|
||||
tags: ["System"],
|
||||
operationId: "getSystemInfo",
|
||||
responses: {
|
||||
200: {
|
||||
description: "System information with enabled capabilities",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(systemInfoResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
11
apps/server/src/modules/system/system.service.ts
Normal file
11
apps/server/src/modules/system/system.service.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { getCapabilities } from "../../core/capabilities";
|
||||
|
||||
const getSystemInfo = async () => {
|
||||
return {
|
||||
capabilities: await getCapabilities(),
|
||||
};
|
||||
};
|
||||
|
||||
export const systemService = {
|
||||
getSystemInfo,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import Docker from "dockerode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import slugify from "slugify";
|
||||
import { getCapabilities } from "../../core/capabilities";
|
||||
import { db } from "../../db/db";
|
||||
import { volumesTable } from "../../db/schema";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
@@ -229,26 +230,37 @@ const getContainersUsingVolume = async (name: string) => {
|
||||
throw new NotFoundError("Volume not found");
|
||||
}
|
||||
|
||||
const docker = new Docker();
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
|
||||
const usingContainers = [];
|
||||
for (const info of containers) {
|
||||
const container = docker.getContainer(info.Id);
|
||||
const inspect = await container.inspect();
|
||||
const mounts = inspect.Mounts || [];
|
||||
const usesVolume = mounts.some((mount) => mount.Type === "volume" && mount.Name === `im-${volume.name}`);
|
||||
if (usesVolume) {
|
||||
usingContainers.push({
|
||||
id: inspect.Id,
|
||||
name: inspect.Name,
|
||||
state: inspect.State.Status,
|
||||
image: inspect.Config.Image,
|
||||
});
|
||||
}
|
||||
const { docker } = await getCapabilities();
|
||||
if (!docker) {
|
||||
logger.debug("Docker capability not available, returning empty containers list");
|
||||
return { containers: [] };
|
||||
}
|
||||
|
||||
return { containers: usingContainers };
|
||||
try {
|
||||
const docker = new Docker();
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
|
||||
const usingContainers = [];
|
||||
for (const info of containers) {
|
||||
const container = docker.getContainer(info.Id);
|
||||
const inspect = await container.inspect();
|
||||
const mounts = inspect.Mounts || [];
|
||||
const usesVolume = mounts.some((mount) => mount.Type === "volume" && mount.Name === `im-${volume.name}`);
|
||||
if (usesVolume) {
|
||||
usingContainers.push({
|
||||
id: inspect.Id,
|
||||
name: inspect.Name,
|
||||
state: inspect.State.Status,
|
||||
image: inspect.Config.Image,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { containers: usingContainers };
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get containers using volume: ${toMessage(error)}`);
|
||||
return { containers: [] };
|
||||
}
|
||||
};
|
||||
|
||||
const listFiles = async (name: string, subPath?: string) => {
|
||||
|
||||
Reference in New Issue
Block a user