Re structured lib and core

This commit is contained in:
2026-01-02 19:51:20 +01:00
parent 3f2d562299
commit caa1191736
26 changed files with 416 additions and 420 deletions
+1 -5
View File
@@ -1,11 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import testAuth from "../auth"; import testAuth from "../auth";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { import { installApp, getApplications, removeApp } from "@/core/application";
installApp,
getApplications,
removeApp,
} from "@/core/server/application";
export async function GET(req: NextRequest): Promise<NextResponse> { export async function GET(req: NextRequest): Promise<NextResponse> {
const auth = await testAuth(req); const auth = await testAuth(req);
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import testAuth from "../auth"; import testAuth from "../auth";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { getDocker, removeContainer } from "@/core/server/docker"; import { getDocker, removeContainer } from "@/core/docker";
import { Docker } from "@/lib/types"; import { Docker } from "@/lib/types";
export async function GET(req: NextRequest): Promise<NextResponse> { export async function GET(req: NextRequest): Promise<NextResponse> {
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import testAuth from "../../auth"; import testAuth from "../../auth";
import { StatusCodes } from "http-status-codes"; 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<NextResponse> { export async function POST(req: NextRequest): Promise<NextResponse> {
const auth = await testAuth(req); const auth = await testAuth(req);
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import testAuth from "../../auth"; import testAuth from "../../auth";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { getContainerStats } from "@/core/server/docker"; import { getContainerStats } from "@/core/docker";
export async function GET(req: NextRequest): Promise<NextResponse> { export async function GET(req: NextRequest): Promise<NextResponse> {
const auth = await testAuth(req); const auth = await testAuth(req);
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import testAuth from "../auth"; import testAuth from "../auth";
import { StatusCodes } from "http-status-codes"; 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<NextResponse> { export async function GET(req: NextRequest): Promise<NextResponse> {
const auth = await testAuth(req); const auth = await testAuth(req);
+1 -1
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import testAuth from "../auth"; import testAuth from "../auth";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import { getAllContainerLogs } from "@/core/server/docker"; import { getAllContainerLogs } from "@/core/docker";
export async function GET(req: NextRequest): Promise<NextResponse> { export async function GET(req: NextRequest): Promise<NextResponse> {
const auth = await testAuth(req); const auth = await testAuth(req);
+1 -1
View File
@@ -14,7 +14,7 @@ import { Button } from "@/components/ui/button";
import { MoreVertical, Plus, Rocket } from "lucide-react"; import { MoreVertical, Plus, Rocket } from "lucide-react";
import InstallApplicationDialog from "@/components/dialogs/InstallApplication"; import InstallApplicationDialog from "@/components/dialogs/InstallApplication";
import { useEffect, useState } from "react"; 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 RemoveDialog from "@/components/dialogs/Remove";
import { Application } from "@/lib/types"; import { Application } from "@/lib/types";
+2 -2
View File
@@ -14,8 +14,8 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Application, Deployment } from "@/lib/types"; import { Application, Deployment } from "@/lib/types";
import { getDeployments } from "@/core/server/deployment"; import { getDeployments } from "@/core/deployment";
import { getApplications } from "@/core/application"; import { getApplications } from "@/lib/dockship/application";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function DeploymentsPage() { export default function DeploymentsPage() {
+1 -1
View File
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Trash2, Eye, EyeOff, Copy, Save, Lock, Loader2 } from "lucide-react"; import { Trash2, Eye, EyeOff, Copy, Save, Lock, Loader2 } from "lucide-react";
import { Env } from "@/lib/types"; 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 { AddVariableDialog } from "@/components/dialogs/AddVariable";
import { import {
DropdownMenu, DropdownMenu,
+1 -1
View File
@@ -8,7 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { Pause, Play, Download, Filter, RefreshCw } from "lucide-react"; import { Pause, Play, Download, Filter, RefreshCw } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Log } from "@/lib/types"; import { Log } from "@/lib/types";
import { getAllContainerLogs } from "@/core/docker"; import { getAllContainerLogs } from "@/lib/dockship/docker";
export default function LogsPage() { export default function LogsPage() {
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
+3 -3
View File
@@ -5,13 +5,13 @@ import { Progress } from "@/components/ui/progress";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { type ChartConfig } from "@/components/ui/chart"; import { type ChartConfig } from "@/components/ui/chart";
import GradientAreaChart from "@/components/GradientAreaChart"; import GradientAreaChart from "@/components/GradientAreaChart";
import { getMetrics } from "@/core/metrics"; import { getMetrics } from "@/lib/dockship/metrics";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useIsMobile } from "@/hooks/use-mobile"; import { useIsMobile } from "@/hooks/use-mobile";
import RadialChart from "@/components/RadialChart"; import RadialChart from "@/components/RadialChart";
import { getLiveNodes, getNodes } from "@/core/node"; import { getLiveNodes, getNodes } from "@/lib/dockship/node";
import { average } from "@/lib/format"; import { average } from "@/lib/format";
import { getContainerStats, getDocker } from "@/core/docker"; import { getContainerStats, getDocker } from "@/lib/dockship/docker";
import { SimpleStats } from "@/lib/types"; import { SimpleStats } from "@/lib/types";
const cpuChartConfig = { const cpuChartConfig = {
+3 -3
View File
@@ -8,11 +8,11 @@ import { Server, Plus, Settings } from "lucide-react";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Docker, Node, NodeLiveData } from "@/lib/types"; import { Docker, Node, NodeLiveData } from "@/lib/types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getLiveNodes } from "@/core/node"; import { getLiveNodes } from "@/lib/dockship/node";
import { formatBytes } from "@/lib/format"; 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 AddNode from "@/components/dialogs/AddNode";
import { getDocker } from "@/core/docker"; import { getDocker } from "@/lib/dockship/docker";
import RemoveDialog from "@/components/dialogs/Remove"; import RemoveDialog from "@/components/dialogs/Remove";
function DiskUsage({ liveData }: NodeLiveData) { function DiskUsage({ liveData }: NodeLiveData) {
+3 -3
View File
@@ -16,11 +16,11 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import Link from "next/link"; import Link from "next/link";
import { getLiveNodes } from "@/core/node"; import { getLiveNodes } from "@/lib/dockship/node";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Application, Docker, Node, NodeLiveData } from "@/lib/types"; import { Application, Docker, Node, NodeLiveData } from "@/lib/types";
import { getDocker } from "@/core/docker"; import { getDocker } from "@/lib/dockship/docker";
import { getApplications } from "@/core/application"; import { getApplications } from "@/lib/dockship/application";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const recentDeployments = [ const recentDeployments = [
+1 -1
View File
@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { GithubIcon } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
+1 -1
View File
@@ -18,7 +18,7 @@ import {
authNode, authNode,
getNodes, getNodes,
setNodes as updateNodes, setNodes as updateNodes,
} from "@/core/node"; } from "@/lib/dockship/node";
export default function AddNode({ children }: { children: ReactNode }) { export default function AddNode({ children }: { children: ReactNode }) {
const [step, setStep] = useState<1 | 2>(1); const [step, setStep] = useState<1 | 2>(1);
@@ -13,10 +13,10 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { getLiveNodes } from "@/core/node"; import { getLiveNodes } from "@/lib/dockship/node";
import { Combobox } from "../ui/combobox"; import { Combobox } from "../ui/combobox";
import { Node, NodeLiveData } from "@/lib/types"; import { Node, NodeLiveData } from "@/lib/types";
import { installApp } from "@/core/application"; import { installApp } from "@/lib/dockship/application";
export default function InstallApplicationDialog({ export default function InstallApplicationDialog({
children, children,
+105 -65
View File
@@ -1,88 +1,128 @@
"use server"; "use server";
import { verifySession } from "./auth/session"; import Docker from "dockerode";
import { getNodes } from "./node"; import { randomUUID } from "crypto";
import axios from "axios"; import fs from "fs";
import { Env, EnvVariable, Application } from "@/lib/types"; 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) { const docker = new Docker();
if (await verifySession()) { const execAsync = util.promisify(exec);
throw new Error("Unauthorized"); const DATA_DIR = path.join(process.cwd(), "data", "apps");
}
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<Application[]> { export async function getApplications(): Promise<Application[]> {
const nodes = await getNodes(); return fs
.readdirSync(DATA_DIR)
.map((appId) =>
JSON.parse(
fs.readFileSync(path.join(DATA_DIR, appId, "app.json")).toString()
)
);
}
return ( export async function installApp(name: string, repo: string, nodeId: string) {
await Promise.all( // Create app ID
nodes.map( const appId = `app_${randomUUID()}`;
async (n) => const appDir = path.join(DATA_DIR, appId);
await ( const repoDir = path.join(appDir, "repo");
await axios.get(`http://${n.ip}:3000/api/applications`, {
headers: { Authorization: `ApiKey ${n.key}` }, // Create directories
}) fs.mkdirSync(repoDir, { recursive: true });
).data.applications
) // Write app.json
) const appConfig: Application = {
).flat(); 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<Record<string, Env>> { export async function getEnv(): Promise<Record<string, Env>> {
const nodes = await getNodes(); const apps = fs.readdirSync(DATA_DIR);
const envRecords = await Promise.all( return apps.reduce((acc, appId) => {
nodes.map(async (n) => { const appPath = path.join(DATA_DIR, appId, "app.json");
const response = await axios.get(`http://${n.ip}:3000/api/env`, { const name = JSON.parse(fs.readFileSync(appPath).toString()).name;
headers: { Authorization: `ApiKey ${n.key}` },
}); const envPath = path.join(DATA_DIR, appId, "env.json");
return response.data as Record<string, Env>; if (fs.existsSync(envPath)) {
}) const envData: Record<string, EnvVariable> = JSON.parse(
fs.readFileSync(envPath, "utf-8")
); );
acc[appId] = { variables: envData, id: appId, name };
const merged: Record<string, Env> = Object.assign({}, ...envRecords); }
return acc;
return merged; }, {} as Record<string, Env>);
} }
export async function setEnv( export async function setEnv(appId: string, variables: EnvVariable[]) {
appId: string, const apps = fs.readdirSync(DATA_DIR);
variables: Record<string, EnvVariable>
) {
const nodes = await getNodes();
nodes.forEach(async (n) => { apps.forEach((id) => {
await axios.post( if (id === appId) {
`http://${n.ip}:3000/api/env`, const envPath = path.join(DATA_DIR, appId, "env.json");
{ appId, variables },
{ fs.writeFileSync(envPath, JSON.stringify(variables));
headers: { Authorization: `ApiKey ${n.key}` },
} }
);
}); });
} }
export async function removeApp(appId: string) { export async function removeApp(appId: string) {
const nodes = await getNodes(); const appPath = path.join(DATA_DIR, appId);
nodes.forEach(async (n) => { const appMeta: Application = JSON.parse(
await axios.delete(`http://${n.ip}:3000/api/applications`, { fs.readFileSync(path.join(appPath, "app.json")).toString()
params: { appId }, );
headers: { Authorization: `ApiKey ${n.key}` },
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 });
} }
+86 -84
View File
@@ -1,110 +1,112 @@
"use server"; "use server";
import axios from "axios"; import { CPUusage } from "@/lib/server/calc";
import { Deployment, Docker, Log, Node, SimpleStats } from "@/lib/types"; import { Log, SimpleStats } from "@/lib/types";
import { verifySession } from "./auth/session"; import Docker from "dockerode";
import { getNodes } from "./node";
export async function getDocker(nodes: Node[]): Promise<Docker[]> { const docker = new Docker();
if (await verifySession()) return [];
return await Promise.all( export async function getDocker() {
nodes.map(async (node) => { const version = await docker.version();
const data = await ( const containers = await docker.listContainers({ all: true });
await axios.get(`http://${node.ip}:3000/api/container/`, { return { version, containers };
headers: { Authorization: `ApiKey ${node.key}` },
})
).data;
return { ...node, containers: data.containers, version: data.version };
})
);
} }
export async function getContainerStats( export async function getContainerStats(
containerId: string containerId: string
): Promise<SimpleStats | undefined> { ): Promise<SimpleStats | undefined> {
const nodes = await getNodes(); const container = docker.getContainer(containerId);
return ( const data = await container.inspect();
await Promise.all(
nodes.map( if (!data.State.Running) {
async (n) => return;
await ( }
await axios.get(`http://${n.ip}:3000/api/container/stats`, {
headers: { Authorization: `ApiKey ${n.key}` }, const stats = await container.stats({ stream: false });
params: { containerId },
}) return {
).data?.stats cpu: CPUusage(stats),
) memory: (stats.memory_stats.usage / stats.memory_stats.limit) * 100,
) };
).find((s) => s);
} }
export async function startContainer(containerId: string) { export async function startContainer(containerId: string) {
const nodes = await getNodes(); await docker.getContainer(containerId).start();
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) { export async function stopContainer(containerId: string) {
const nodes = await getNodes(); await docker.getContainer(containerId).stop();
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) { export async function removeContainer(containerId: string) {
const nodes = await getNodes(); await docker.getContainer(containerId).remove();
}
nodes.forEach( function inferLevel(message: string): "info" | "warn" | "error" {
async (n) => const msg = message.toLowerCase();
await (
await axios.delete(`http://${n.ip}:3000/api/container`, { if (msg.includes("error") || msg.includes("fatal")) return "error";
headers: { Authorization: `ApiKey ${n.key}` }, if (msg.includes("warn")) return "warn";
params: { containerId },
}) return "info";
).data }
);
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<Log[]> {
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<Log[]> { export async function getAllContainerLogs(): Promise<Log[]> {
const nodes = await getNodes(); const containers = await docker.listContainers({ all: true });
return ( const logs = await Promise.all(
await Promise.all( containers.map((c) => getContainerLogs(c.Id, c.Names[0].replace("/", "")))
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()
); );
return logs
.flat()
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
} }
-128
View File
@@ -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<Application[]> {
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<Record<string, Env>> {
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<string, EnvVariable> = JSON.parse(
fs.readFileSync(envPath, "utf-8")
);
acc[appId] = { variables: envData, id: appId, name };
}
return acc;
}, {} as Record<string, Env>);
}
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 });
}
-112
View File
@@ -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<SimpleStats | undefined> {
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<Log[]> {
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<Log[]> {
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));
}
+88
View File
@@ -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<Application[]> {
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<Record<string, Env>> {
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<string, Env>;
})
);
const merged: Record<string, Env> = Object.assign({}, ...envRecords);
return merged;
}
export async function setEnv(
appId: string,
variables: Record<string, EnvVariable>
) {
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}` },
});
});
}
+110
View File
@@ -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<Docker[]> {
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<SimpleStats | undefined> {
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<Log[]> {
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()
);
}
@@ -3,7 +3,7 @@
import { read, write } from "@/app/api/file"; import { read, write } from "@/app/api/file";
import { Node, NodeLiveData } from "@/lib/types"; import { Node, NodeLiveData } from "@/lib/types";
import axios from "axios"; import axios from "axios";
import { verifySession } from "./auth/session"; import { verifySession } from "../../core/auth/session";
export async function getNodes(): Promise<Node[]> { export async function getNodes(): Promise<Node[]> {
if (await verifySession()) return []; if (await verifySession()) return [];