From caa1191736a9659c9ddeef8b2ad041bb208e2b0b Mon Sep 17 00:00:00 2001 From: Leo dev Date: Fri, 2 Jan 2026 19:51:20 +0100 Subject: [PATCH] Re structured lib and core --- src/app/api/applications/route.ts | 6 +- src/app/api/container/route.ts | 2 +- src/app/api/container/start/route.ts | 2 +- src/app/api/container/stats/route.ts | 2 +- src/app/api/env/route.ts | 2 +- src/app/api/logs/route.ts | 2 +- src/app/applications/page.tsx | 2 +- src/app/deployments/page.tsx | 4 +- src/app/environment/page.tsx | 2 +- src/app/logs/page.tsx | 2 +- src/app/monitoring/page.tsx | 6 +- src/app/nodes/page.tsx | 6 +- src/app/overview/page.tsx | 6 +- src/app/page.tsx | 2 +- src/components/dialogs/AddNode.tsx | 2 +- src/components/dialogs/InstallApplication.tsx | 4 +- src/core/application.ts | 174 +++++++++++------- src/core/{server => }/deployment.ts | 0 src/core/docker.ts | 170 ++++++++--------- src/core/server/application.ts | 128 ------------- src/core/server/docker.ts | 112 ----------- src/lib/dockship/application.ts | 88 +++++++++ src/lib/dockship/docker.ts | 110 +++++++++++ src/{core => lib/dockship}/metrics.ts | 0 src/{core => lib/dockship}/node.ts | 2 +- src/{core/auth => lib/dockship}/redirect.ts | 0 26 files changed, 416 insertions(+), 420 deletions(-) rename src/core/{server => }/deployment.ts (100%) delete mode 100644 src/core/server/application.ts delete mode 100644 src/core/server/docker.ts create mode 100644 src/lib/dockship/application.ts create mode 100644 src/lib/dockship/docker.ts rename src/{core => lib/dockship}/metrics.ts (100%) rename src/{core => lib/dockship}/node.ts (95%) rename src/{core/auth => lib/dockship}/redirect.ts (100%) diff --git a/src/app/api/applications/route.ts b/src/app/api/applications/route.ts index 85d40a9..6dab73d 100644 --- a/src/app/api/applications/route.ts +++ b/src/app/api/applications/route.ts @@ -1,11 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; -import { - installApp, - getApplications, - removeApp, -} from "@/core/server/application"; +import { installApp, getApplications, removeApp } from "@/core/application"; export async function GET(req: NextRequest): Promise { const auth = await testAuth(req); diff --git a/src/app/api/container/route.ts b/src/app/api/container/route.ts index 0651447..8deb02f 100644 --- a/src/app/api/container/route.ts +++ b/src/app/api/container/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; -import { getDocker, removeContainer } from "@/core/server/docker"; +import { getDocker, removeContainer } from "@/core/docker"; import { Docker } from "@/lib/types"; export async function GET(req: NextRequest): Promise { diff --git a/src/app/api/container/start/route.ts b/src/app/api/container/start/route.ts index b0475c6..d593d28 100644 --- a/src/app/api/container/start/route.ts +++ b/src/app/api/container/start/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../../auth"; import { StatusCodes } from "http-status-codes"; -import { startContainer, stopContainer } from "@/core/server/docker"; +import { startContainer, stopContainer } from "@/core/docker"; export async function POST(req: NextRequest): Promise { const auth = await testAuth(req); diff --git a/src/app/api/container/stats/route.ts b/src/app/api/container/stats/route.ts index f775bf8..324b83a 100644 --- a/src/app/api/container/stats/route.ts +++ b/src/app/api/container/stats/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../../auth"; import { StatusCodes } from "http-status-codes"; -import { getContainerStats } from "@/core/server/docker"; +import { getContainerStats } from "@/core/docker"; export async function GET(req: NextRequest): Promise { const auth = await testAuth(req); diff --git a/src/app/api/env/route.ts b/src/app/api/env/route.ts index 249ce8b..bff743a 100644 --- a/src/app/api/env/route.ts +++ b/src/app/api/env/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; -import { getEnv, setEnv } from "@/core/server/application"; +import { getEnv, setEnv } from "@/core/application"; export async function GET(req: NextRequest): Promise { const auth = await testAuth(req); diff --git a/src/app/api/logs/route.ts b/src/app/api/logs/route.ts index 23dd21e..2c4d920 100644 --- a/src/app/api/logs/route.ts +++ b/src/app/api/logs/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; -import { getAllContainerLogs } from "@/core/server/docker"; +import { getAllContainerLogs } from "@/core/docker"; export async function GET(req: NextRequest): Promise { const auth = await testAuth(req); diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx index 2003549..c85bf59 100644 --- a/src/app/applications/page.tsx +++ b/src/app/applications/page.tsx @@ -14,7 +14,7 @@ import { Button } from "@/components/ui/button"; import { MoreVertical, Plus, Rocket } from "lucide-react"; import InstallApplicationDialog from "@/components/dialogs/InstallApplication"; import { useEffect, useState } from "react"; -import { getApplications, removeApp } from "@/core/application"; +import { getApplications, removeApp } from "@/lib/dockship/application"; import RemoveDialog from "@/components/dialogs/Remove"; import { Application } from "@/lib/types"; diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index ebd8cbe..1d16d08 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -14,8 +14,8 @@ import { } from "lucide-react"; import { useEffect, useState } from "react"; import { Application, Deployment } from "@/lib/types"; -import { getDeployments } from "@/core/server/deployment"; -import { getApplications } from "@/core/application"; +import { getDeployments } from "@/core/deployment"; +import { getApplications } from "@/lib/dockship/application"; import { cn } from "@/lib/utils"; export default function DeploymentsPage() { diff --git a/src/app/environment/page.tsx b/src/app/environment/page.tsx index d051c0b..2db907e 100644 --- a/src/app/environment/page.tsx +++ b/src/app/environment/page.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Trash2, Eye, EyeOff, Copy, Save, Lock, Loader2 } from "lucide-react"; import { Env } from "@/lib/types"; -import { getEnv, setEnv } from "@/core/application"; +import { getEnv, setEnv } from "@/lib/dockship/application"; import { AddVariableDialog } from "@/components/dialogs/AddVariable"; import { DropdownMenu, diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx index 514a9d4..9509bc0 100644 --- a/src/app/logs/page.tsx +++ b/src/app/logs/page.tsx @@ -8,7 +8,7 @@ import { Badge } from "@/components/ui/badge"; import { Pause, Play, Download, Filter, RefreshCw } from "lucide-react"; import { cn } from "@/lib/utils"; import { Log } from "@/lib/types"; -import { getAllContainerLogs } from "@/core/docker"; +import { getAllContainerLogs } from "@/lib/dockship/docker"; export default function LogsPage() { const [isPaused, setIsPaused] = useState(false); diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index 7c95127..74b3a30 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -5,13 +5,13 @@ import { Progress } from "@/components/ui/progress"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { type ChartConfig } from "@/components/ui/chart"; import GradientAreaChart from "@/components/GradientAreaChart"; -import { getMetrics } from "@/core/metrics"; +import { getMetrics } from "@/lib/dockship/metrics"; import { useEffect, useState } from "react"; import { useIsMobile } from "@/hooks/use-mobile"; import RadialChart from "@/components/RadialChart"; -import { getLiveNodes, getNodes } from "@/core/node"; +import { getLiveNodes, getNodes } from "@/lib/dockship/node"; import { average } from "@/lib/format"; -import { getContainerStats, getDocker } from "@/core/docker"; +import { getContainerStats, getDocker } from "@/lib/dockship/docker"; import { SimpleStats } from "@/lib/types"; const cpuChartConfig = { diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 21328c9..7beaa07 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -8,11 +8,11 @@ import { Server, Plus, Settings } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import { Docker, Node, NodeLiveData } from "@/lib/types"; import { useEffect, useState } from "react"; -import { getLiveNodes } from "@/core/node"; +import { getLiveNodes } from "@/lib/dockship/node"; import { formatBytes } from "@/lib/format"; -import { setNodes as updateNodes } from "@/core/node"; +import { setNodes as updateNodes } from "@/lib/dockship/node"; import AddNode from "@/components/dialogs/AddNode"; -import { getDocker } from "@/core/docker"; +import { getDocker } from "@/lib/dockship/docker"; import RemoveDialog from "@/components/dialogs/Remove"; function DiskUsage({ liveData }: NodeLiveData) { diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index fdd9a3f..103395b 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -16,11 +16,11 @@ import { } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import Link from "next/link"; -import { getLiveNodes } from "@/core/node"; +import { getLiveNodes } from "@/lib/dockship/node"; import { useEffect, useState } from "react"; import { Application, Docker, Node, NodeLiveData } from "@/lib/types"; -import { getDocker } from "@/core/docker"; -import { getApplications } from "@/core/application"; +import { getDocker } from "@/lib/dockship/docker"; +import { getApplications } from "@/lib/dockship/application"; import { cn } from "@/lib/utils"; const recentDeployments = [ diff --git a/src/app/page.tsx b/src/app/page.tsx index 6749ac3..3626446 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import redirectAuth from "@/core/auth/redirect"; +import redirectAuth from "@/lib/dockship/redirect"; import { GithubIcon } from "lucide-react"; import Image from "next/image"; diff --git a/src/components/dialogs/AddNode.tsx b/src/components/dialogs/AddNode.tsx index ace0d4a..ac9bae9 100644 --- a/src/components/dialogs/AddNode.tsx +++ b/src/components/dialogs/AddNode.tsx @@ -18,7 +18,7 @@ import { authNode, getNodes, setNodes as updateNodes, -} from "@/core/node"; +} from "@/lib/dockship/node"; export default function AddNode({ children }: { children: ReactNode }) { const [step, setStep] = useState<1 | 2>(1); diff --git a/src/components/dialogs/InstallApplication.tsx b/src/components/dialogs/InstallApplication.tsx index 3260ba7..47b242c 100644 --- a/src/components/dialogs/InstallApplication.tsx +++ b/src/components/dialogs/InstallApplication.tsx @@ -13,10 +13,10 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { getLiveNodes } from "@/core/node"; +import { getLiveNodes } from "@/lib/dockship/node"; import { Combobox } from "../ui/combobox"; import { Node, NodeLiveData } from "@/lib/types"; -import { installApp } from "@/core/application"; +import { installApp } from "@/lib/dockship/application"; export default function InstallApplicationDialog({ children, diff --git a/src/core/application.ts b/src/core/application.ts index 8718382..6d5b58b 100644 --- a/src/core/application.ts +++ b/src/core/application.ts @@ -1,88 +1,128 @@ "use server"; -import { verifySession } from "./auth/session"; -import { getNodes } from "./node"; -import axios from "axios"; -import { Env, EnvVariable, Application } from "@/lib/types"; +import Docker from "dockerode"; +import { randomUUID } from "crypto"; +import fs from "fs"; +import path from "path"; +import { exec } from "child_process"; +import util from "util"; +import { Application, Env, EnvVariable } from "@/lib/types"; -export async function installApp(name: string, repo: string, nodeId: string) { - if (await verifySession()) { - throw new Error("Unauthorized"); - } - - const nodes = await getNodes(); - - const targetNode = nodes.find((node) => node.node_id === nodeId); - - return await ( - await axios.post( - `http://${targetNode?.ip}:3000/api/applications`, - { name, repo, nodeId }, - { - headers: { Authorization: `ApiKey ${targetNode?.key}` }, - } - ) - ).data.applications; -} +const docker = new Docker(); +const execAsync = util.promisify(exec); +const DATA_DIR = path.join(process.cwd(), "data", "apps"); export async function getApplications(): Promise { - const nodes = await getNodes(); - - return ( - await Promise.all( - nodes.map( - async (n) => - await ( - await axios.get(`http://${n.ip}:3000/api/applications`, { - headers: { Authorization: `ApiKey ${n.key}` }, - }) - ).data.applications + return fs + .readdirSync(DATA_DIR) + .map((appId) => + JSON.parse( + fs.readFileSync(path.join(DATA_DIR, appId, "app.json")).toString() ) - ) - ).flat(); + ); +} + +export async function installApp(name: string, repo: string, nodeId: string) { + // Create app ID + const appId = `app_${randomUUID()}`; + const appDir = path.join(DATA_DIR, appId); + const repoDir = path.join(appDir, "repo"); + + // Create directories + fs.mkdirSync(repoDir, { recursive: true }); + + // Write app.json + const appConfig: Application = { + id: appId, + name, + repo, + nodeId, + createdAt: new Date().toISOString(), + deployments: [], + }; + + fs.writeFileSync( + path.join(appDir, "app.json"), + JSON.stringify(appConfig, null, 2) + ); + + // Write env.json + fs.writeFileSync(path.join(appDir, "env.json"), JSON.stringify([], null, 2)); + + // Clone repository + try { + await execAsync(`git clone ${repo} ${repoDir}`); + } catch (err) { + throw new Error("Failed to clone repository"); + } + + // Validate Dockerfile + const dockerfilePath = path.join(repoDir, "Dockerfile"); + + try { + fs.accessSync(dockerfilePath); + } catch { + fs.rmSync(appDir, { recursive: true }); + throw new Error("Dockerfile not found in repository root"); + } + + return { + success: true, + appId, + }; } export async function getEnv(): Promise> { - const nodes = await getNodes(); + const apps = fs.readdirSync(DATA_DIR); - const envRecords = await Promise.all( - nodes.map(async (n) => { - const response = await axios.get(`http://${n.ip}:3000/api/env`, { - headers: { Authorization: `ApiKey ${n.key}` }, - }); - return response.data as Record; - }) - ); + return apps.reduce((acc, appId) => { + const appPath = path.join(DATA_DIR, appId, "app.json"); + const name = JSON.parse(fs.readFileSync(appPath).toString()).name; - const merged: Record = Object.assign({}, ...envRecords); - - return merged; + const envPath = path.join(DATA_DIR, appId, "env.json"); + if (fs.existsSync(envPath)) { + const envData: Record = JSON.parse( + fs.readFileSync(envPath, "utf-8") + ); + acc[appId] = { variables: envData, id: appId, name }; + } + return acc; + }, {} as Record); } -export async function setEnv( - appId: string, - variables: Record -) { - const nodes = await getNodes(); +export async function setEnv(appId: string, variables: EnvVariable[]) { + const apps = fs.readdirSync(DATA_DIR); - nodes.forEach(async (n) => { - await axios.post( - `http://${n.ip}:3000/api/env`, - { appId, variables }, - { - headers: { Authorization: `ApiKey ${n.key}` }, - } - ); + apps.forEach((id) => { + if (id === appId) { + const envPath = path.join(DATA_DIR, appId, "env.json"); + + fs.writeFileSync(envPath, JSON.stringify(variables)); + } }); } export async function removeApp(appId: string) { - const nodes = await getNodes(); + const appPath = path.join(DATA_DIR, appId); - nodes.forEach(async (n) => { - await axios.delete(`http://${n.ip}:3000/api/applications`, { - params: { appId }, - headers: { Authorization: `ApiKey ${n.key}` }, - }); + const appMeta: Application = JSON.parse( + fs.readFileSync(path.join(appPath, "app.json")).toString() + ); + + appMeta.deployments.forEach(async (imgId) => { + // Remove containers + (await docker.listContainers({ all: true })) + .filter((con) => con.ImageID === imgId) + .forEach((con) => { + const container = docker.getContainer(con.Id); + container.stop(); + container.remove(); + }); + + // Remove image + const image = docker.getImage(imgId); + image.remove(); }); + + fs.rmSync(appPath, { recursive: true }); } diff --git a/src/core/server/deployment.ts b/src/core/deployment.ts similarity index 100% rename from src/core/server/deployment.ts rename to src/core/deployment.ts diff --git a/src/core/docker.ts b/src/core/docker.ts index 8906e56..e8e203a 100644 --- a/src/core/docker.ts +++ b/src/core/docker.ts @@ -1,110 +1,112 @@ "use server"; -import axios from "axios"; -import { Deployment, Docker, Log, Node, SimpleStats } from "@/lib/types"; -import { verifySession } from "./auth/session"; -import { getNodes } from "./node"; +import { CPUusage } from "@/lib/server/calc"; +import { Log, SimpleStats } from "@/lib/types"; +import Docker from "dockerode"; -export async function getDocker(nodes: Node[]): Promise { - if (await verifySession()) return []; +const docker = new Docker(); - return await Promise.all( - nodes.map(async (node) => { - const data = await ( - await axios.get(`http://${node.ip}:3000/api/container/`, { - headers: { Authorization: `ApiKey ${node.key}` }, - }) - ).data; - - return { ...node, containers: data.containers, version: data.version }; - }) - ); +export async function getDocker() { + const version = await docker.version(); + const containers = await docker.listContainers({ all: true }); + return { version, containers }; } export async function getContainerStats( containerId: string ): Promise { - const nodes = await getNodes(); + const container = docker.getContainer(containerId); - 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?.stats - ) - ) - ).find((s) => s); + const data = await container.inspect(); + + if (!data.State.Running) { + return; + } + + const stats = await container.stats({ stream: false }); + + return { + cpu: CPUusage(stats), + memory: (stats.memory_stats.usage / stats.memory_stats.limit) * 100, + }; } 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 - ); + await docker.getContainer(containerId).start(); } 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 - ); + await docker.getContainer(containerId).stop(); } export async function removeContainer(containerId: string) { - const nodes = await getNodes(); + await docker.getContainer(containerId).remove(); +} - nodes.forEach( - async (n) => - await ( - await axios.delete(`http://${n.ip}:3000/api/container`, { - headers: { Authorization: `ApiKey ${n.key}` }, - params: { containerId }, - }) - ).data - ); +function inferLevel(message: string): "info" | "warn" | "error" { + const msg = message.toLowerCase(); + + if (msg.includes("error") || msg.includes("fatal")) return "error"; + if (msg.includes("warn")) return "warn"; + + return "info"; +} + +function demuxLogs(buffer: Buffer): string { + let offset = 0; + let output = ""; + + while (offset < buffer.length) { + const streamType = buffer[offset]; + const length = buffer.readUInt32BE(offset + 4); + offset += 8; + + output += buffer.slice(offset, offset + length).toString("utf8"); + offset += length; + } + + return output; +} + +async function getContainerLogs( + containerId: string, + name: string +): Promise { + const container = docker.getContainer(containerId); + + const raw = await container.logs({ + stdout: true, + stderr: true, + timestamps: true, + }); + + const text = demuxLogs(raw); + + return text + .split("\n") + .filter(Boolean) + .map((line) => { + const firstSpace = line.indexOf(" "); + const timestamp = line.slice(0, firstSpace); + const message = line.slice(firstSpace + 1); + + return { + timestamp, + level: inferLevel(message), + source: name, + message, + } satisfies Log; + }); } export async function getAllContainerLogs(): Promise { - const nodes = await getNodes(); + const containers = await docker.listContainers({ all: true }); - return ( - await Promise.all( - nodes.map( - async (n) => - await ( - await axios.get(`http://${n.ip}:3000/api/logs`, { - headers: { Authorization: `ApiKey ${n.key}` }, - }) - ).data - ) - ) - ) + const logs = await Promise.all( + containers.map((c) => getContainerLogs(c.Id, c.Names[0].replace("/", ""))) + ); + + return logs .flat() - .sort( - (a, b) => - new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() - ); + .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)); } diff --git a/src/core/server/application.ts b/src/core/server/application.ts deleted file mode 100644 index 6d5b58b..0000000 --- a/src/core/server/application.ts +++ /dev/null @@ -1,128 +0,0 @@ -"use server"; - -import Docker from "dockerode"; -import { randomUUID } from "crypto"; -import fs from "fs"; -import path from "path"; -import { exec } from "child_process"; -import util from "util"; -import { Application, Env, EnvVariable } from "@/lib/types"; - -const docker = new Docker(); -const execAsync = util.promisify(exec); -const DATA_DIR = path.join(process.cwd(), "data", "apps"); - -export async function getApplications(): Promise { - return fs - .readdirSync(DATA_DIR) - .map((appId) => - JSON.parse( - fs.readFileSync(path.join(DATA_DIR, appId, "app.json")).toString() - ) - ); -} - -export async function installApp(name: string, repo: string, nodeId: string) { - // Create app ID - const appId = `app_${randomUUID()}`; - const appDir = path.join(DATA_DIR, appId); - const repoDir = path.join(appDir, "repo"); - - // Create directories - fs.mkdirSync(repoDir, { recursive: true }); - - // Write app.json - const appConfig: Application = { - id: appId, - name, - repo, - nodeId, - createdAt: new Date().toISOString(), - deployments: [], - }; - - fs.writeFileSync( - path.join(appDir, "app.json"), - JSON.stringify(appConfig, null, 2) - ); - - // Write env.json - fs.writeFileSync(path.join(appDir, "env.json"), JSON.stringify([], null, 2)); - - // Clone repository - try { - await execAsync(`git clone ${repo} ${repoDir}`); - } catch (err) { - throw new Error("Failed to clone repository"); - } - - // Validate Dockerfile - const dockerfilePath = path.join(repoDir, "Dockerfile"); - - try { - fs.accessSync(dockerfilePath); - } catch { - fs.rmSync(appDir, { recursive: true }); - throw new Error("Dockerfile not found in repository root"); - } - - return { - success: true, - appId, - }; -} - -export async function getEnv(): Promise> { - const apps = fs.readdirSync(DATA_DIR); - - return apps.reduce((acc, appId) => { - const appPath = path.join(DATA_DIR, appId, "app.json"); - const name = JSON.parse(fs.readFileSync(appPath).toString()).name; - - const envPath = path.join(DATA_DIR, appId, "env.json"); - if (fs.existsSync(envPath)) { - const envData: Record = JSON.parse( - fs.readFileSync(envPath, "utf-8") - ); - acc[appId] = { variables: envData, id: appId, name }; - } - return acc; - }, {} as Record); -} - -export async function setEnv(appId: string, variables: EnvVariable[]) { - const apps = fs.readdirSync(DATA_DIR); - - apps.forEach((id) => { - if (id === appId) { - const envPath = path.join(DATA_DIR, appId, "env.json"); - - fs.writeFileSync(envPath, JSON.stringify(variables)); - } - }); -} - -export async function removeApp(appId: string) { - const appPath = path.join(DATA_DIR, appId); - - const appMeta: Application = JSON.parse( - fs.readFileSync(path.join(appPath, "app.json")).toString() - ); - - appMeta.deployments.forEach(async (imgId) => { - // Remove containers - (await docker.listContainers({ all: true })) - .filter((con) => con.ImageID === imgId) - .forEach((con) => { - const container = docker.getContainer(con.Id); - container.stop(); - container.remove(); - }); - - // Remove image - const image = docker.getImage(imgId); - image.remove(); - }); - - fs.rmSync(appPath, { recursive: true }); -} diff --git a/src/core/server/docker.ts b/src/core/server/docker.ts deleted file mode 100644 index e8e203a..0000000 --- a/src/core/server/docker.ts +++ /dev/null @@ -1,112 +0,0 @@ -"use server"; - -import { CPUusage } from "@/lib/server/calc"; -import { Log, SimpleStats } from "@/lib/types"; -import Docker from "dockerode"; - -const docker = new Docker(); - -export async function getDocker() { - const version = await docker.version(); - const containers = await docker.listContainers({ all: true }); - return { version, containers }; -} - -export async function getContainerStats( - containerId: string -): Promise { - const container = docker.getContainer(containerId); - - const data = await container.inspect(); - - if (!data.State.Running) { - return; - } - - const stats = await container.stats({ stream: false }); - - return { - cpu: CPUusage(stats), - memory: (stats.memory_stats.usage / stats.memory_stats.limit) * 100, - }; -} - -export async function startContainer(containerId: string) { - await docker.getContainer(containerId).start(); -} - -export async function stopContainer(containerId: string) { - await docker.getContainer(containerId).stop(); -} - -export async function removeContainer(containerId: string) { - await docker.getContainer(containerId).remove(); -} - -function inferLevel(message: string): "info" | "warn" | "error" { - const msg = message.toLowerCase(); - - if (msg.includes("error") || msg.includes("fatal")) return "error"; - if (msg.includes("warn")) return "warn"; - - return "info"; -} - -function demuxLogs(buffer: Buffer): string { - let offset = 0; - let output = ""; - - while (offset < buffer.length) { - const streamType = buffer[offset]; - const length = buffer.readUInt32BE(offset + 4); - offset += 8; - - output += buffer.slice(offset, offset + length).toString("utf8"); - offset += length; - } - - return output; -} - -async function getContainerLogs( - containerId: string, - name: string -): Promise { - const container = docker.getContainer(containerId); - - const raw = await container.logs({ - stdout: true, - stderr: true, - timestamps: true, - }); - - const text = demuxLogs(raw); - - return text - .split("\n") - .filter(Boolean) - .map((line) => { - const firstSpace = line.indexOf(" "); - const timestamp = line.slice(0, firstSpace); - const message = line.slice(firstSpace + 1); - - return { - timestamp, - level: inferLevel(message), - source: name, - message, - } satisfies Log; - }); -} - -export async function getAllContainerLogs(): Promise { - const containers = await docker.listContainers({ all: true }); - - const logs = await Promise.all( - containers.map((c) => getContainerLogs(c.Id, c.Names[0].replace("/", ""))) - ); - - return logs - .flat() - .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)); -} diff --git a/src/lib/dockship/application.ts b/src/lib/dockship/application.ts new file mode 100644 index 0000000..9ac56ea --- /dev/null +++ b/src/lib/dockship/application.ts @@ -0,0 +1,88 @@ +"use server"; + +import { verifySession } from "../../core/auth/session"; +import { getNodes } from "@/lib/dockship/node"; +import axios from "axios"; +import { Env, EnvVariable, Application } from "@/lib/types"; + +export async function installApp(name: string, repo: string, nodeId: string) { + if (await verifySession()) { + throw new Error("Unauthorized"); + } + + const nodes = await getNodes(); + + const targetNode = nodes.find((node) => node.node_id === nodeId); + + return await ( + await axios.post( + `http://${targetNode?.ip}:3000/api/applications`, + { name, repo, nodeId }, + { + headers: { Authorization: `ApiKey ${targetNode?.key}` }, + } + ) + ).data.applications; +} + +export async function getApplications(): Promise { + const nodes = await getNodes(); + + return ( + await Promise.all( + nodes.map( + async (n) => + await ( + await axios.get(`http://${n.ip}:3000/api/applications`, { + headers: { Authorization: `ApiKey ${n.key}` }, + }) + ).data.applications + ) + ) + ).flat(); +} + +export async function getEnv(): Promise> { + const nodes = await getNodes(); + + const envRecords = await Promise.all( + nodes.map(async (n) => { + const response = await axios.get(`http://${n.ip}:3000/api/env`, { + headers: { Authorization: `ApiKey ${n.key}` }, + }); + return response.data as Record; + }) + ); + + const merged: Record = Object.assign({}, ...envRecords); + + return merged; +} + +export async function setEnv( + appId: string, + variables: Record +) { + const nodes = await getNodes(); + + nodes.forEach(async (n) => { + await axios.post( + `http://${n.ip}:3000/api/env`, + { appId, variables }, + { + headers: { Authorization: `ApiKey ${n.key}` }, + } + ); + }); +} + +export async function removeApp(appId: string) { + const nodes = await getNodes(); + + nodes.forEach(async (n) => { + await axios.delete(`http://${n.ip}:3000/api/applications`, { + params: { appId }, + headers: { Authorization: `ApiKey ${n.key}` }, + }); + }); +} diff --git a/src/lib/dockship/docker.ts b/src/lib/dockship/docker.ts new file mode 100644 index 0000000..a827927 --- /dev/null +++ b/src/lib/dockship/docker.ts @@ -0,0 +1,110 @@ +"use server"; + +import axios from "axios"; +import { Deployment, Docker, Log, Node, SimpleStats } from "@/lib/types"; +import { verifySession } from "../../core/auth/session"; +import { getNodes } from "./node"; + +export async function getDocker(nodes: Node[]): Promise { + if (await verifySession()) return []; + + return await Promise.all( + nodes.map(async (node) => { + const data = await ( + await axios.get(`http://${node.ip}:3000/api/container/`, { + headers: { Authorization: `ApiKey ${node.key}` }, + }) + ).data; + + return { ...node, containers: data.containers, version: data.version }; + }) + ); +} + +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?.stats + ) + ) + ).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 + ); +} + +export async function removeContainer(containerId: string) { + const nodes = await getNodes(); + + nodes.forEach( + async (n) => + await ( + await axios.delete(`http://${n.ip}:3000/api/container`, { + headers: { Authorization: `ApiKey ${n.key}` }, + params: { containerId }, + }) + ).data + ); +} + +export async function getAllContainerLogs(): Promise { + const nodes = await getNodes(); + + return ( + await Promise.all( + nodes.map( + async (n) => + await ( + await axios.get(`http://${n.ip}:3000/api/logs`, { + headers: { Authorization: `ApiKey ${n.key}` }, + }) + ).data + ) + ) + ) + .flat() + .sort( + (a, b) => + new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); +} diff --git a/src/core/metrics.ts b/src/lib/dockship/metrics.ts similarity index 100% rename from src/core/metrics.ts rename to src/lib/dockship/metrics.ts diff --git a/src/core/node.ts b/src/lib/dockship/node.ts similarity index 95% rename from src/core/node.ts rename to src/lib/dockship/node.ts index 98ab7a6..17229e0 100644 --- a/src/core/node.ts +++ b/src/lib/dockship/node.ts @@ -3,7 +3,7 @@ import { read, write } from "@/app/api/file"; import { Node, NodeLiveData } from "@/lib/types"; import axios from "axios"; -import { verifySession } from "./auth/session"; +import { verifySession } from "../../core/auth/session"; export async function getNodes(): Promise { if (await verifySession()) return []; diff --git a/src/core/auth/redirect.ts b/src/lib/dockship/redirect.ts similarity index 100% rename from src/core/auth/redirect.ts rename to src/lib/dockship/redirect.ts