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
+107 -67
View File
@@ -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 });
}
-14
View File
@@ -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
View File
@@ -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));
}
-67
View File
@@ -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;
}
-47
View File
@@ -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 };
}
-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));
}