Re structured lib and core
This commit is contained in:
+107
-67
@@ -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<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
|
||||
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<Record<string, Env>> {
|
||||
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<string, Env>;
|
||||
})
|
||||
);
|
||||
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<string, Env> = Object.assign({}, ...envRecords);
|
||||
|
||||
return merged;
|
||||
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: Record<string, EnvVariable>
|
||||
) {
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { client } from "@/core/auth/oauth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function redirectAuth() {
|
||||
const authorizationUri = client.authorizeURL({
|
||||
redirect_uri: "http://localhost:3000/api/auth/callback",
|
||||
scope: "user:email",
|
||||
state: "random_state_string",
|
||||
});
|
||||
|
||||
redirect(authorizationUri);
|
||||
}
|
||||
+86
-84
@@ -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<Docker[]> {
|
||||
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<SimpleStats | undefined> {
|
||||
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<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 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));
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { Node } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
import { getNodes } from "./node";
|
||||
|
||||
export async function getMetrics(): Promise<
|
||||
{
|
||||
time: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
in: number;
|
||||
out: number;
|
||||
}[]
|
||||
> {
|
||||
const nodes = await getNodes();
|
||||
const nodeMetrics = await Promise.all(nodes.map(getNodeMetrics));
|
||||
|
||||
if (nodeMetrics.length === 0) return [];
|
||||
|
||||
const numNodes = nodeMetrics.length;
|
||||
const numPoints = nodeMetrics[0].length;
|
||||
|
||||
const averaged: (typeof nodeMetrics)[0] = [];
|
||||
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const sum = nodeMetrics.reduce(
|
||||
(acc, metrics) => {
|
||||
const m = metrics[i];
|
||||
acc.cpu += m.cpu;
|
||||
acc.memory += m.memory;
|
||||
acc.in += m.in;
|
||||
acc.out += m.out;
|
||||
return acc;
|
||||
},
|
||||
{ cpu: 0, memory: 0, in: 0, out: 0 }
|
||||
);
|
||||
|
||||
averaged.push({
|
||||
time: nodeMetrics[0][i].time,
|
||||
cpu: sum.cpu / numNodes,
|
||||
memory: sum.memory / numNodes,
|
||||
in: sum.in / numNodes,
|
||||
out: sum.out / numNodes,
|
||||
});
|
||||
}
|
||||
|
||||
return averaged;
|
||||
}
|
||||
|
||||
export async function getNodeMetrics(node: Node): Promise<
|
||||
{
|
||||
time: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
in: number;
|
||||
out: number;
|
||||
}[]
|
||||
> {
|
||||
const data = await (
|
||||
await axios.get(`http://${node.ip}:3000/api/metrics`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
})
|
||||
).data;
|
||||
|
||||
return data.metrics;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { read, write } from "@/app/api/file";
|
||||
import { Node, NodeLiveData } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
import { verifySession } from "./auth/session";
|
||||
|
||||
export async function getNodes(): Promise<Node[]> {
|
||||
if (await verifySession()) return [];
|
||||
|
||||
return await read(() => [], "data", "nodes.json");
|
||||
}
|
||||
|
||||
export async function setNodes(nodes: Node[]) {
|
||||
if (await verifySession()) return;
|
||||
|
||||
await write(nodes, "data", "nodes.json");
|
||||
}
|
||||
|
||||
export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> {
|
||||
if (await verifySession()) return [];
|
||||
|
||||
const nodes: Node[] = await read(() => [], "data", "nodes.json");
|
||||
|
||||
return await Promise.all(
|
||||
nodes.map(async (node) => ({
|
||||
...node,
|
||||
liveData: await (
|
||||
await axios.get(`http://${node.ip}:3000/api/status`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
})
|
||||
).data.liveData,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export async function authNode(ip: string, key: string) {
|
||||
if (await verifySession()) return;
|
||||
|
||||
const data = await (
|
||||
await axios.get(`http://${ip}:3000/api/auth`, {
|
||||
headers: { Authorization: `ApiKey ${key}` },
|
||||
})
|
||||
).data;
|
||||
|
||||
return { version: data.version, node_id: data.node_id, os: data.os };
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user