Docker Compose projeleri için tam kapsamlı yönetim paneli ve otomatik deployment altyapısı eklendi. Sistem özellikleri: - Belirtilen root dizin altındaki docker-compose dosyası içeren projeleri tarama - Git repo bağlantısı ile branch yönetimi ve klonlama/pull işlemleri - Docker compose up/down komutları ile otomatik deploy - Gitea webhook entegrasyonu ile commit bazlı tetikleme - Deploy geçmişi, log kayıtları ve durum takibi (running/success/failed) - Deploy metrikleri ve dashboard görselleştirmesi - Webhook token ve secret yönetimi ile güvenlik - Proje favicon servisi Teknik değişiklikler: - Backend: deploymentProject, deploymentRun ve settings modelleri eklendi - Backend: deploymentService ile git ve docker işlemleri otomatize edildi - Backend: webhook doğrulaması için signature kontrolü eklendi - Docker: docker-cli ve docker-compose bağımlılıkları eklendi - Frontend: deployments ve settings sayfaları eklendi - Frontend: dashboard'a deploy metrikleri ve aktivite akışı eklendi - API: /api/deployments ve /api/settings yolları eklendi
194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import { Router } from "express";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
import { authMiddleware } from "../middleware/authMiddleware.js";
|
||
import { deploymentService } from "../services/deploymentService.js";
|
||
import { DeploymentProject } from "../models/deploymentProject.js";
|
||
import { DeploymentRun } from "../models/deploymentRun.js";
|
||
|
||
const router = Router();
|
||
|
||
const faviconCandidates = [
|
||
"favicon.ico",
|
||
"public/favicon.ico",
|
||
"public/favicon.png",
|
||
"public/favicon.svg",
|
||
"assets/favicon.ico"
|
||
];
|
||
|
||
function getContentType(filePath: string) {
|
||
if (filePath.endsWith(".svg")) return "image/svg+xml";
|
||
if (filePath.endsWith(".png")) return "image/png";
|
||
return "image/x-icon";
|
||
}
|
||
|
||
router.get("/:id/favicon", async (req, res) => {
|
||
const { id } = req.params;
|
||
const project = await DeploymentProject.findById(id).lean();
|
||
if (!project) return res.status(404).end();
|
||
const rootPath = path.resolve(project.rootPath);
|
||
|
||
for (const candidate of faviconCandidates) {
|
||
const filePath = path.join(rootPath, candidate);
|
||
if (!fs.existsSync(filePath)) continue;
|
||
res.setHeader("Content-Type", getContentType(filePath));
|
||
res.setHeader("Cache-Control", "public, max-age=300");
|
||
return fs.createReadStream(filePath).pipe(res);
|
||
}
|
||
|
||
return res.status(404).end();
|
||
});
|
||
|
||
router.get("/scan", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
try {
|
||
const candidates = await deploymentService.scanRoot();
|
||
return res.json(candidates);
|
||
} catch (err) {
|
||
return res.status(500).json({ message: "Root taraması yapılamadı" });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.get("/branches", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const repoUrl = req.query.repoUrl as string | undefined;
|
||
if (!repoUrl) {
|
||
return res.status(400).json({ message: "repoUrl gerekli" });
|
||
}
|
||
try {
|
||
const branches = await deploymentService.listRemoteBranches(repoUrl);
|
||
return res.json({ branches });
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Branch listesi alınamadı", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.get("/metrics/summary", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const since = new Date();
|
||
since.setDate(since.getDate() - 7);
|
||
|
||
const dailyStats = await DeploymentRun.aggregate([
|
||
{ $match: { startedAt: { $gte: since } } },
|
||
{
|
||
$group: {
|
||
_id: { $dateToString: { format: "%Y-%m-%d", date: "$startedAt" } },
|
||
total: { $sum: 1 },
|
||
success: {
|
||
$sum: {
|
||
$cond: [{ $eq: ["$status", "success"] }, 1, 0]
|
||
}
|
||
},
|
||
failed: {
|
||
$sum: {
|
||
$cond: [{ $eq: ["$status", "failed"] }, 1, 0]
|
||
}
|
||
},
|
||
avgDurationMs: { $avg: "$durationMs" }
|
||
}
|
||
},
|
||
{ $sort: { _id: 1 } }
|
||
]);
|
||
|
||
const recentRuns = await DeploymentRun.find()
|
||
.sort({ startedAt: -1 })
|
||
.limit(10)
|
||
.populate("project", "name repoUrl rootPath")
|
||
.lean();
|
||
return res.json({ recentRuns, dailyStats });
|
||
});
|
||
});
|
||
|
||
router.get("/", async (_req, res) => {
|
||
authMiddleware(_req, res, async () => {
|
||
const projects = await DeploymentProject.find().sort({ createdAt: -1 }).lean();
|
||
return res.json(projects);
|
||
});
|
||
});
|
||
|
||
router.get("/:id", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
const project = await DeploymentProject.findById(id).lean();
|
||
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
const runs = await DeploymentRun.find({ project: id })
|
||
.sort({ startedAt: -1 })
|
||
.limit(20)
|
||
.lean();
|
||
return res.json({ project, runs });
|
||
});
|
||
});
|
||
|
||
router.post("/", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { name, rootPath, repoUrl, branch, composeFile, port } = req.body;
|
||
if (!name || !rootPath || !repoUrl || !branch || !composeFile) {
|
||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||
}
|
||
try {
|
||
const created = await deploymentService.createProject({
|
||
name,
|
||
rootPath,
|
||
repoUrl,
|
||
branch,
|
||
composeFile,
|
||
port
|
||
});
|
||
return res.status(201).json(created);
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Deployment oluşturulamadı", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.put("/:id", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
const { name, repoUrl, branch, composeFile, port } = req.body;
|
||
if (!name || !repoUrl || !branch || !composeFile) {
|
||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||
}
|
||
try {
|
||
const updated = await deploymentService.updateProject(id, {
|
||
name,
|
||
repoUrl,
|
||
branch,
|
||
composeFile,
|
||
port
|
||
});
|
||
if (!updated) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
return res.json(updated);
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Deployment güncellenemedi", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.delete("/:id", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
try {
|
||
const deleted = await DeploymentProject.findByIdAndDelete(id);
|
||
if (!deleted) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
await DeploymentRun.deleteMany({ project: id });
|
||
return res.json({ success: true });
|
||
} catch (err) {
|
||
return res.status(400).json({ message: "Deployment silinemedi", error: (err as Error).message });
|
||
}
|
||
});
|
||
});
|
||
|
||
router.post("/:id/run", async (req, res) => {
|
||
authMiddleware(req, res, async () => {
|
||
const { id } = req.params;
|
||
const project = await DeploymentProject.findById(id);
|
||
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||
deploymentService.runDeployment(id).catch(() => undefined);
|
||
return res.json({ queued: true });
|
||
});
|
||
});
|
||
|
||
export default router;
|