diff --git a/src/app/api/get-docker/route.ts b/src/app/api/container/route.ts similarity index 100% rename from src/app/api/get-docker/route.ts rename to src/app/api/container/route.ts diff --git a/src/app/api/container/start/route.ts b/src/app/api/container/start/route.ts new file mode 100644 index 0000000..b0475c6 --- /dev/null +++ b/src/app/api/container/start/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from "next/server"; +import testAuth from "../../auth"; +import { StatusCodes } from "http-status-codes"; +import { startContainer, stopContainer } from "@/core/server/docker"; + +export async function POST(req: NextRequest): Promise { + const auth = await testAuth(req); + const id = req.nextUrl.searchParams.get("containerId"); + + if (!id) { + return NextResponse.json( + { message: "Invalid containerId param" }, + { status: StatusCodes.BAD_REQUEST } + ); + } + + if (auth) { + return NextResponse.json( + { message: auth }, + { status: StatusCodes.UNAUTHORIZED } + ); + } + + await startContainer(id); + + return NextResponse.json({ + message: "ok", + }); +} + +export async function DELETE(req: NextRequest): Promise { + const auth = await testAuth(req); + const id = req.nextUrl.searchParams.get("containerId"); + + if (!id) { + return NextResponse.json( + { message: "Invalid containerId param" }, + { status: StatusCodes.BAD_REQUEST } + ); + } + + if (auth) { + return NextResponse.json( + { message: auth }, + { status: StatusCodes.UNAUTHORIZED } + ); + } + + await stopContainer(id); + + return NextResponse.json({ + message: "ok", + }); +} diff --git a/src/app/api/container/stats/route.ts b/src/app/api/container/stats/route.ts new file mode 100644 index 0000000..e30ce89 --- /dev/null +++ b/src/app/api/container/stats/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from "next/server"; +import testAuth from "../../auth"; +import { StatusCodes } from "http-status-codes"; +import { getContainerStats } from "@/core/server/docker"; + +export async function GET(req: NextRequest): Promise { + const auth = await testAuth(req); + const id = req.nextUrl.searchParams.get("containerId"); + + if (!id) { + return NextResponse.json( + { message: "Invalid containerId param" }, + { status: StatusCodes.BAD_REQUEST } + ); + } + + if (auth) { + return NextResponse.json( + { message: auth }, + { status: StatusCodes.UNAUTHORIZED } + ); + } + + const stats = await getContainerStats(id); + + return NextResponse.json({ + message: "ok", + stats: { + cpu_stats: stats?.cpu_stats, + memory_stats: stats?.memory_stats, + }, + }); +} diff --git a/src/app/containers/page.tsx b/src/app/containers/page.tsx index 60c3886..f0fccef 100644 --- a/src/app/containers/page.tsx +++ b/src/app/containers/page.tsx @@ -4,18 +4,17 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - MoreVertical, - Play, - Square, - Trash2, - Container, -} from "lucide-react"; +import { MoreVertical, Play, Square, Trash2, Container } from "lucide-react"; import { useEffect, useState } from "react"; -import { Docker } from "@/lib/types"; import { getDocker } from "@/core/docker"; import { getNodes } from "@/core/node"; import { ContainerInfo } from "dockerode"; +import { formatBytes } from "@/lib/format"; +import { + getContainerStats, + startContainer, + stopContainer, +} from "@/core/docker"; type ContainerSummary = { id: string; @@ -24,16 +23,16 @@ type ContainerSummary = { node: string; status: "running" | "stopped"; uptime: string; - cpu: string; + cpu: number; memory: string; }; -function summarizeContainer( +async function summarizeContainer( container: ContainerInfo, nodeName: string -): ContainerSummary { +): Promise { // 1. Shorten the long container ID - const shortId = container.Id.slice(0, 12); + const shortId = container.Id; // 2. Clean the name const name = container.Names[0]?.replace(/^\//, "") ?? "unknown"; @@ -44,9 +43,13 @@ function summarizeContainer( // 4. Compute uptime from Status string (if available) const uptime = container.Status?.replace(/^Up\s*/, "") ?? "unknown"; - // 5. Placeholder for CPU and memory - const cpu = "4%"; - const memory = "64MB / 128MB"; + const stats = await getContainerStats(container.Id); + + // 5. CPU and memory + const cpu = stats?.cpu_stats?.cpu_usage?.total_usage || 0; + const memory = `${formatBytes( + stats?.memory_stats?.usage || 0 + )} / ${formatBytes(stats?.memory_stats?.max_usage || 0)}`; return { id: shortId, @@ -69,8 +72,12 @@ export default function ContainersPage() { const dockerNodes = await getDocker(nodes); setContainers( - nodes.flatMap((node, i) => - dockerNodes[i].containers.map((c) => summarizeContainer(c, node.name)) + await Promise.all( + nodes.flatMap((node, i) => + dockerNodes[i].containers.map((c) => + summarizeContainer(c, node.name) + ) + ) ) ); }; @@ -131,7 +138,7 @@ export default function ContainersPage() { {container.name}
- {container.id} + {container.id.slice(0, 12)}
@@ -160,11 +167,19 @@ export default function ContainersPage() {
{container.status === "running" ? ( - ) : ( - )} diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 20c683c..e6007c9 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -120,7 +120,11 @@ export default function OverviewPage() { /> sum + n.containers.length, 0)} + value={dockerNodes.reduce( + (sum, n) => + sum + n.containers.filter((c) => c.State === "running").length, + 0 + )} subtitle="running" icon={} /> diff --git a/src/core/docker.ts b/src/core/docker.ts index 6ee9137..051031f 100644 --- a/src/core/docker.ts +++ b/src/core/docker.ts @@ -3,6 +3,8 @@ import axios from "axios"; import { Docker, Node } from "@/lib/types"; import { verifySession } from "./auth/session"; +import { ContainerStats } from "dockerode"; +import { getNodes } from "./node"; export async function getDocker(nodes: Node[]): Promise { if (await verifySession()) return []; @@ -10,7 +12,7 @@ export async function getDocker(nodes: Node[]): Promise { return await Promise.all( nodes.map(async (node) => { const data = await ( - await axios.get(`http://${node.ip}:3000/api/get-docker`, { + await axios.get(`http://${node.ip}:3000/api/container/`, { headers: { Authorization: `ApiKey ${node.key}` }, }) ).data; @@ -19,3 +21,55 @@ export async function getDocker(nodes: Node[]): Promise { }) ); } + +export async function getContainerStats( + containerId: string +): Promise { + const nodes = await getNodes(); + + return ( + await Promise.all( + nodes.map( + async (n) => + await ( + await axios.get(`http://${n.ip}:3000/api/container/stats`, { + headers: { Authorization: `ApiKey ${n.key}` }, + params: { containerId }, + }) + ).data + ) + ) + ).find((s) => s); +} + +export async function startContainer(containerId: string) { + const nodes = await getNodes(); + + nodes.forEach( + async (n) => + await ( + await axios.post( + `http://${n.ip}:3000/api/container/start`, + { containerId }, + { + headers: { Authorization: `ApiKey ${n.key}` }, + params: { containerId }, + } + ) + ).data + ); +} + +export async function stopContainer(containerId: string) { + const nodes = await getNodes(); + + nodes.forEach( + async (n) => + await ( + await axios.delete(`http://${n.ip}:3000/api/container/start`, { + headers: { Authorization: `ApiKey ${n.key}` }, + params: { containerId }, + }) + ).data + ); +} diff --git a/src/core/server/docker.ts b/src/core/server/docker.ts index fdb79a2..cc2b768 100644 --- a/src/core/server/docker.ts +++ b/src/core/server/docker.ts @@ -1,11 +1,25 @@ "use server"; -import Docker from "dockerode"; +import Docker, { ContainerStats } from "dockerode"; const docker = new Docker(); export async function getDocker() { const version = await docker.version(); - const containers = await docker.listContainers(); + const containers = await docker.listContainers({ all: true }); return { version, containers }; } + +export async function getContainerStats( + containerId: string +): Promise { + return await (await docker.getContainer(containerId)).stats(); +} + +export async function startContainer(containerId: string) { + await docker.getContainer(containerId).start(); +} + +export async function stopContainer(containerId: string) { + await docker.getContainer(containerId).stop(); +}