feat(settings): otomatik docker image temizliği ekle
Docker image temizliği için yapılandırılabilir zamanlayıcı ve manuel tetikleme özelliği eklenmiştir. Kullanıcılar saat, gün veya hafta bazlı periyotlar belirleyebilir ve anlık temizlik yapabilir.
This commit is contained in:
@@ -126,6 +126,7 @@ async function start() {
|
|||||||
try {
|
try {
|
||||||
await mongoose.connect(config.mongoUri);
|
await mongoose.connect(config.mongoUri);
|
||||||
console.log("MongoDB'ye bağlanıldı");
|
console.log("MongoDB'ye bağlanıldı");
|
||||||
|
await deploymentService.ensureSettings();
|
||||||
await jobService.bootstrapFromFilesystem();
|
await jobService.bootstrapFromFilesystem();
|
||||||
await jobService.bootstrap();
|
await jobService.bootstrap();
|
||||||
await deploymentService.normalizeExistingCommitMessages();
|
await deploymentService.normalizeExistingCommitMessages();
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import mongoose, { Schema, Document } from "mongoose";
|
|||||||
export interface SettingsDocument extends Document {
|
export interface SettingsDocument extends Document {
|
||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
|
cleanupIntervalValue?: number;
|
||||||
|
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -10,7 +12,9 @@ export interface SettingsDocument extends Document {
|
|||||||
const SettingsSchema = new Schema<SettingsDocument>(
|
const SettingsSchema = new Schema<SettingsDocument>(
|
||||||
{
|
{
|
||||||
webhookToken: { type: String, required: true },
|
webhookToken: { type: String, required: true },
|
||||||
webhookSecret: { type: String, required: true }
|
webhookSecret: { type: String, required: true },
|
||||||
|
cleanupIntervalValue: { type: Number, min: 1 },
|
||||||
|
cleanupIntervalUnit: { type: String, enum: ["saat", "gün", "hafta"] }
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ router.get("/", async (_req, res) => {
|
|||||||
return res.json({
|
return res.json({
|
||||||
webhookToken: settings.webhookToken,
|
webhookToken: settings.webhookToken,
|
||||||
webhookSecret: settings.webhookSecret,
|
webhookSecret: settings.webhookSecret,
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit,
|
||||||
updatedAt: settings.updatedAt
|
updatedAt: settings.updatedAt
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,4 +33,29 @@ router.post("/secret/rotate", async (_req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/cleanup-interval", async (req, res) => {
|
||||||
|
const settings = await deploymentService.ensureSettings();
|
||||||
|
const { value, unit } = req.body as {
|
||||||
|
value?: number;
|
||||||
|
unit?: "saat" | "gün" | "hafta";
|
||||||
|
};
|
||||||
|
if (!value || value < 1 || !unit) {
|
||||||
|
return res.status(400).json({ message: "Geçerli periyot gerekli" });
|
||||||
|
}
|
||||||
|
settings.cleanupIntervalValue = value;
|
||||||
|
settings.cleanupIntervalUnit = unit;
|
||||||
|
await settings.save();
|
||||||
|
await deploymentService.updateCleanupSchedule(value, unit);
|
||||||
|
return res.json({
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit,
|
||||||
|
updatedAt: settings.updatedAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/cleanup-images", async (_req, res) => {
|
||||||
|
await deploymentService.cleanupUnusedImages();
|
||||||
|
return res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ type DeploymentMetadata = {
|
|||||||
type SettingsMetadata = {
|
type SettingsMetadata = {
|
||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
|
cleanupIntervalValue?: number;
|
||||||
|
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
|
||||||
};
|
};
|
||||||
|
|
||||||
type StoredRun = {
|
type StoredRun = {
|
||||||
@@ -360,6 +362,7 @@ async function runCompose(project: DeploymentProjectDocument, onData: (line: str
|
|||||||
class DeploymentService {
|
class DeploymentService {
|
||||||
private running: Map<string, boolean> = new Map();
|
private running: Map<string, boolean> = new Map();
|
||||||
private io: Server | null = null;
|
private io: Server | null = null;
|
||||||
|
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
setSocket(io: Server) {
|
setSocket(io: Server) {
|
||||||
this.io = io;
|
this.io = io;
|
||||||
@@ -451,14 +454,23 @@ class DeploymentService {
|
|||||||
|
|
||||||
async ensureSettings() {
|
async ensureSettings() {
|
||||||
const existing = await Settings.findOne();
|
const existing = await Settings.findOne();
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
await this.updateCleanupSchedule(existing.cleanupIntervalValue, existing.cleanupIntervalUnit);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const fileSettings = await readSettingsFile();
|
const fileSettings = await readSettingsFile();
|
||||||
if (fileSettings) {
|
if (fileSettings) {
|
||||||
const createdFromFile = await Settings.create({
|
const createdFromFile = await Settings.create({
|
||||||
webhookToken: fileSettings.webhookToken,
|
webhookToken: fileSettings.webhookToken,
|
||||||
webhookSecret: fileSettings.webhookSecret
|
webhookSecret: fileSettings.webhookSecret,
|
||||||
|
cleanupIntervalValue: fileSettings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: fileSettings.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
|
await this.updateCleanupSchedule(
|
||||||
|
createdFromFile.cleanupIntervalValue,
|
||||||
|
createdFromFile.cleanupIntervalUnit
|
||||||
|
);
|
||||||
return createdFromFile;
|
return createdFromFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,8 +480,11 @@ class DeploymentService {
|
|||||||
});
|
});
|
||||||
await writeSettingsFile({
|
await writeSettingsFile({
|
||||||
webhookToken: created.webhookToken,
|
webhookToken: created.webhookToken,
|
||||||
webhookSecret: created.webhookSecret
|
webhookSecret: created.webhookSecret,
|
||||||
|
cleanupIntervalValue: created.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: created.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
|
await this.updateCleanupSchedule(created.cleanupIntervalValue, created.cleanupIntervalUnit);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +494,9 @@ class DeploymentService {
|
|||||||
await settings.save();
|
await settings.save();
|
||||||
await writeSettingsFile({
|
await writeSettingsFile({
|
||||||
webhookToken: settings.webhookToken,
|
webhookToken: settings.webhookToken,
|
||||||
webhookSecret: settings.webhookSecret
|
webhookSecret: settings.webhookSecret,
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
@@ -490,11 +507,35 @@ class DeploymentService {
|
|||||||
await settings.save();
|
await settings.save();
|
||||||
await writeSettingsFile({
|
await writeSettingsFile({
|
||||||
webhookToken: settings.webhookToken,
|
webhookToken: settings.webhookToken,
|
||||||
webhookSecret: settings.webhookSecret
|
webhookSecret: settings.webhookSecret,
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCleanupSchedule(value?: number, unit?: "saat" | "gün" | "hafta") {
|
||||||
|
if (this.cleanupTimer) {
|
||||||
|
clearInterval(this.cleanupTimer);
|
||||||
|
this.cleanupTimer = null;
|
||||||
|
}
|
||||||
|
if (!value || !unit) return;
|
||||||
|
const intervalMs =
|
||||||
|
unit === "saat"
|
||||||
|
? value * 60 * 60 * 1000
|
||||||
|
: unit === "gün"
|
||||||
|
? value * 24 * 60 * 60 * 1000
|
||||||
|
: value * 7 * 24 * 60 * 60 * 1000;
|
||||||
|
if (!intervalMs || Number.isNaN(intervalMs)) return;
|
||||||
|
this.cleanupTimer = setInterval(() => {
|
||||||
|
this.cleanupUnusedImages().catch(() => undefined);
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupUnusedImages() {
|
||||||
|
await runCommand("docker image prune -a -f", process.cwd(), () => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
async createProject(input: {
|
async createProject(input: {
|
||||||
name: string;
|
name: string;
|
||||||
repoUrl: string;
|
repoUrl: string;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { apiClient } from "./client";
|
|||||||
export interface SettingsResponse {
|
export interface SettingsResponse {
|
||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
|
cleanupIntervalValue?: number;
|
||||||
|
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,3 +22,13 @@ export async function rotateWebhookSecret(): Promise<SettingsResponse> {
|
|||||||
const { data } = await apiClient.post("/settings/secret/rotate");
|
const { data } = await apiClient.post("/settings/secret/rotate");
|
||||||
return data as SettingsResponse;
|
return data as SettingsResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveCleanupInterval(value: number, unit: "saat" | "gün" | "hafta") {
|
||||||
|
const { data } = await apiClient.post("/settings/cleanup-interval", { value, unit });
|
||||||
|
return data as SettingsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupImages(): Promise<{ success: boolean }> {
|
||||||
|
const { data } = await apiClient.post("/settings/cleanup-images");
|
||||||
|
return data as { success: boolean };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCopy, faEye, faEyeSlash, faRotate } from "@fortawesome/free-solid-svg-icons";
|
import { faBroom, faCopy, faEye, faEyeSlash, faFloppyDisk, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
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 { fetchSettings, rotateWebhookSecret, rotateWebhookToken, SettingsResponse } from "../api/settings";
|
import { Input } from "../components/ui/input";
|
||||||
|
import { Label } from "../components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
|
||||||
|
import {
|
||||||
|
cleanupImages,
|
||||||
|
fetchSettings,
|
||||||
|
rotateWebhookSecret,
|
||||||
|
rotateWebhookToken,
|
||||||
|
saveCleanupInterval,
|
||||||
|
SettingsResponse
|
||||||
|
} from "../api/settings";
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const [settings, setSettings] = useState<SettingsResponse | null>(null);
|
const [settings, setSettings] = useState<SettingsResponse | null>(null);
|
||||||
@@ -13,10 +23,22 @@ export function SettingsPage() {
|
|||||||
const [rotatingSecret, setRotatingSecret] = useState(false);
|
const [rotatingSecret, setRotatingSecret] = useState(false);
|
||||||
const [showToken, setShowToken] = useState(false);
|
const [showToken, setShowToken] = useState(false);
|
||||||
const [showSecret, setShowSecret] = useState(false);
|
const [showSecret, setShowSecret] = useState(false);
|
||||||
|
const [cleanupValue, setCleanupValue] = useState<string>("1");
|
||||||
|
const [cleanupUnit, setCleanupUnit] = useState<"saat" | "gün" | "hafta">("hafta");
|
||||||
|
const [savingCleanup, setSavingCleanup] = useState(false);
|
||||||
|
const [cleaning, setCleaning] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSettings()
|
fetchSettings()
|
||||||
.then((data) => setSettings(data))
|
.then((data) => {
|
||||||
|
setSettings(data);
|
||||||
|
if (data.cleanupIntervalValue) {
|
||||||
|
setCleanupValue(String(data.cleanupIntervalValue));
|
||||||
|
}
|
||||||
|
if (data.cleanupIntervalUnit) {
|
||||||
|
setCleanupUnit(data.cleanupIntervalUnit);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => toast.error("Settings yüklenemedi"))
|
.catch(() => toast.error("Settings yüklenemedi"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -77,6 +99,36 @@ export function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveCleanup = async () => {
|
||||||
|
const value = Number(cleanupValue);
|
||||||
|
if (!value || Number.isNaN(value) || value < 1) {
|
||||||
|
toast.error("Geçerli bir periyot girin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSavingCleanup(true);
|
||||||
|
try {
|
||||||
|
const data = await saveCleanupInterval(value, cleanupUnit);
|
||||||
|
setSettings((prev) => (prev ? { ...prev, ...data } : data));
|
||||||
|
toast.success("Temizlik periyodu kaydedildi");
|
||||||
|
} catch {
|
||||||
|
toast.error("Periyot kaydedilemedi");
|
||||||
|
} finally {
|
||||||
|
setSavingCleanup(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCleanupImages = async () => {
|
||||||
|
setCleaning(true);
|
||||||
|
try {
|
||||||
|
await cleanupImages();
|
||||||
|
toast.success("Kullanılmayan image'lar temizlendi");
|
||||||
|
} catch {
|
||||||
|
toast.error("Temizlik başarısız");
|
||||||
|
} finally {
|
||||||
|
setCleaning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
||||||
@@ -138,6 +190,58 @@ export function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Image Temizliği</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cleanupValue">Temizlik Periyodu</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="cleanupValue"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={cleanupValue}
|
||||||
|
onChange={(e) => setCleanupValue(e.target.value)}
|
||||||
|
className="bg-white"
|
||||||
|
/>
|
||||||
|
<Select value={cleanupUnit} onValueChange={(value) => setCleanupUnit(value as typeof cleanupUnit)}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="Birim" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="saat">saat</SelectItem>
|
||||||
|
<SelectItem value="gün">gün</SelectItem>
|
||||||
|
<SelectItem value="hafta">hafta</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleSaveCleanup}
|
||||||
|
disabled={savingCleanup}
|
||||||
|
className="bg-white"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFloppyDisk} className="mr-2 h-4 w-4" />
|
||||||
|
Kaydet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCleanupImages}
|
||||||
|
disabled={cleaning}
|
||||||
|
className="bg-rose-100 text-rose-700 hover:bg-rose-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBroom} className="mr-2 h-4 w-4" />
|
||||||
|
Clean Cache Images
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Webhook Secret</CardTitle>
|
<CardTitle>Webhook Secret</CardTitle>
|
||||||
|
|||||||
Reference in New Issue
Block a user