mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: display containers using the volume
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
deleteVolume,
|
||||
getVolume,
|
||||
updateVolume,
|
||||
getContainersUsingVolume,
|
||||
mountVolume,
|
||||
unmountVolume,
|
||||
} from "../sdk.gen";
|
||||
@@ -23,6 +24,7 @@ import type {
|
||||
GetVolumeData,
|
||||
UpdateVolumeData,
|
||||
UpdateVolumeResponse,
|
||||
GetContainersUsingVolumeData,
|
||||
MountVolumeData,
|
||||
MountVolumeResponse,
|
||||
UnmountVolumeData,
|
||||
@@ -226,6 +228,27 @@ export const updateVolumeMutation = (
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getContainersUsingVolumeQueryKey = (options: Options<GetContainersUsingVolumeData>) =>
|
||||
createQueryKey("getContainersUsingVolume", options);
|
||||
|
||||
/**
|
||||
* Get containers using a volume by name
|
||||
*/
|
||||
export const getContainersUsingVolumeOptions = (options: Options<GetContainersUsingVolumeData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await getContainersUsingVolume({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getContainersUsingVolumeQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
export const mountVolumeQueryKey = (options: Options<MountVolumeData>) => createQueryKey("mountVolume", options);
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,9 @@ import type {
|
||||
UpdateVolumeData,
|
||||
UpdateVolumeResponses,
|
||||
UpdateVolumeErrors,
|
||||
GetContainersUsingVolumeData,
|
||||
GetContainersUsingVolumeResponses,
|
||||
GetContainersUsingVolumeErrors,
|
||||
MountVolumeData,
|
||||
MountVolumeResponses,
|
||||
MountVolumeErrors,
|
||||
@@ -122,6 +125,22 @@ export const updateVolume = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get containers using a volume by name
|
||||
*/
|
||||
export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(
|
||||
options: Options<GetContainersUsingVolumeData, ThrowOnError>,
|
||||
) => {
|
||||
return (options.client ?? _heyApiClient).get<
|
||||
GetContainersUsingVolumeResponses,
|
||||
GetContainersUsingVolumeErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/v1/volumes/{name}/containers",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Mount a volume
|
||||
*/
|
||||
|
||||
@@ -349,6 +349,39 @@ export type UpdateVolumeResponses = {
|
||||
|
||||
export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeResponses];
|
||||
|
||||
export type GetContainersUsingVolumeData = {
|
||||
body?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
query?: never;
|
||||
url: "/api/v1/volumes/{name}/containers";
|
||||
};
|
||||
|
||||
export type GetContainersUsingVolumeErrors = {
|
||||
/**
|
||||
* Volume not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type GetContainersUsingVolumeResponses = {
|
||||
/**
|
||||
* List of containers using the volume
|
||||
*/
|
||||
200: {
|
||||
containers: Array<{
|
||||
id: string;
|
||||
image: string;
|
||||
name: string;
|
||||
state: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetContainersUsingVolumeResponse =
|
||||
GetContainersUsingVolumeResponses[keyof GetContainersUsingVolumeResponses];
|
||||
|
||||
export type MountVolumeData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as YML from "yaml";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { CodeBlock } from "~/components/ui/code-block";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||
import type { Volume } from "~/lib/types";
|
||||
import * as YML from "yaml";
|
||||
import { getContainersUsingVolumeOptions } from "../../../api-client/@tanstack/react-query.gen";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
@@ -24,8 +27,24 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
|
||||
const dockerRunCommand = `docker run -v ${volume.name}:/path/in/container nginx:latest`;
|
||||
|
||||
const containersQuery = getContainersUsingVolumeOptions({ path: { name: volume.name } });
|
||||
const { data: containersData, isLoading, error } = useQuery(containersQuery);
|
||||
|
||||
const containers = containersData?.containers || [];
|
||||
|
||||
const getStateClass = (state: string) => {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "bg-green-100 text-green-800";
|
||||
case "exited":
|
||||
return "bg-orange-100 text-orange-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]">
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,_1fr)_minmax(0,_1fr)]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Plug-and-play Docker integration</CardTitle>
|
||||
@@ -55,11 +74,45 @@ export const DockerTabContent = ({ volume }: Props) => {
|
||||
<div className="grid">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Best practices</CardTitle>
|
||||
<CardDescription>Validate the automation before enabling it in production.</CardDescription>
|
||||
<CardTitle>Containers Using This Volume</CardTitle>
|
||||
<CardDescription>List of Docker containers mounting this volume.</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4 text-sm"></CardContent>
|
||||
<CardContent className="space-y-4 text-sm">
|
||||
{isLoading && <div>Loading containers...</div>}
|
||||
{error && <div className="text-destructive">Failed to load containers: {String(error)}</div>}
|
||||
{!isLoading && !error && containers.length === 0 && (
|
||||
<div>No containers are currently using this volume.</div>
|
||||
)}
|
||||
{!isLoading && !error && containers.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Image</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{containers.map((container) => (
|
||||
<TableRow key={container.id}>
|
||||
<TableCell>{container.name}</TableCell>
|
||||
<TableCell>{container.id.slice(0, 12)}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${getStateClass(container.state)}`}
|
||||
>
|
||||
{container.state}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{container.image}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@ironmount/schemas": "workspace:*",
|
||||
"@scalar/hono-api-reference": "^0.9.13",
|
||||
"arktype": "^2.1.20",
|
||||
"dockerode": "^4.0.8",
|
||||
"dotenv": "^17.2.1",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"hono": "^4.9.2",
|
||||
@@ -22,6 +23,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.20",
|
||||
"@types/dockerode": "^3.3.44",
|
||||
"drizzle-kit": "^0.31.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import { getTasks, schedule } from "node-cron";
|
||||
import { db } from "../../db/db";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { volumesTable } from "../../db/schema";
|
||||
import { schedule, getTasks } from "node-cron";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { volumeService } from "../volumes/volume.service";
|
||||
|
||||
export const startup = async () => {
|
||||
@@ -18,7 +18,7 @@ export const startup = async () => {
|
||||
}
|
||||
|
||||
const existingTasks = getTasks();
|
||||
existingTasks.forEach((task) => task.destroy());
|
||||
existingTasks.forEach(async (task) => await task.destroy());
|
||||
|
||||
schedule("* * * * *", async () => {
|
||||
logger.info("Running health check for all volumes...");
|
||||
|
||||
@@ -4,16 +4,18 @@ import {
|
||||
createVolumeBody,
|
||||
createVolumeDto,
|
||||
deleteVolumeDto,
|
||||
type GetVolumeResponseDto,
|
||||
getContainersDto,
|
||||
getVolumeDto,
|
||||
type ListContainersResponseDto,
|
||||
type ListVolumesResponseDto,
|
||||
listVolumesDto,
|
||||
mountVolumeDto,
|
||||
testConnectionBody,
|
||||
testConnectionDto,
|
||||
unmountVolumeDto,
|
||||
updateVolumeBody,
|
||||
updateVolumeDto,
|
||||
mountVolumeDto,
|
||||
unmountVolumeDto,
|
||||
type GetVolumeResponseDto,
|
||||
} from "./volume.dto";
|
||||
import { volumeService } from "./volume.service";
|
||||
|
||||
@@ -70,6 +72,16 @@ export const volumeController = new Hono()
|
||||
|
||||
return c.json(response, 200);
|
||||
})
|
||||
.get("/:name/containers", getContainersDto, async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const { containers } = await volumeService.getContainersUsingVolume(name);
|
||||
|
||||
const response = {
|
||||
containers,
|
||||
} satisfies ListContainersResponseDto;
|
||||
|
||||
return c.json(response, 200);
|
||||
})
|
||||
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
||||
const { name } = c.req.param();
|
||||
const body = c.req.valid("json");
|
||||
|
||||
@@ -258,3 +258,38 @@ export const unmountVolumeDto = describeRoute({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get containers using a volume
|
||||
*/
|
||||
const containerSchema = type({
|
||||
id: "string",
|
||||
name: "string",
|
||||
state: "string",
|
||||
image: "string",
|
||||
});
|
||||
|
||||
export const listContainersResponse = type({
|
||||
containers: containerSchema.array(),
|
||||
});
|
||||
export type ListContainersResponseDto = typeof listContainersResponse.infer;
|
||||
|
||||
export const getContainersDto = describeRoute({
|
||||
description: "Get containers using a volume by name",
|
||||
operationId: "getContainersUsingVolume",
|
||||
validateResponse: true,
|
||||
tags: ["Volumes"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of containers using the volume",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(listContainersResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Volume not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,17 +2,17 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { BackendConfig } from "@ironmount/schemas";
|
||||
import Docker from "dockerode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import slugify from "slugify";
|
||||
import { config } from "../../core/config";
|
||||
import { VOLUME_MOUNT_BASE } from "../../core/constants";
|
||||
import { db } from "../../db/db";
|
||||
import { volumesTable } from "../../db/schema";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
||||
import { VOLUME_MOUNT_BASE } from "../../core/constants";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
|
||||
const listVolumes = async () => {
|
||||
const volumes = await db.query.volumesTable.findMany({});
|
||||
@@ -192,6 +192,37 @@ const checkHealth = async (name: string) => {
|
||||
return { status, error };
|
||||
};
|
||||
|
||||
const getContainersUsingVolume = async (name: string) => {
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.name, name),
|
||||
});
|
||||
|
||||
if (!volume) {
|
||||
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 === name);
|
||||
if (usesVolume) {
|
||||
usingContainers.push({
|
||||
id: inspect.Id,
|
||||
name: inspect.Name,
|
||||
state: inspect.State.Status,
|
||||
image: inspect.Config.Image,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { containers: usingContainers };
|
||||
};
|
||||
|
||||
export const volumeService = {
|
||||
listVolumes,
|
||||
createVolume,
|
||||
@@ -202,4 +233,5 @@ export const volumeService = {
|
||||
testConnection,
|
||||
unmountVolume,
|
||||
checkHealth,
|
||||
getContainersUsingVolume,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user