diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1c11102 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +PORT=4000 +NODE_ENV=production +PORTAINER_URL=https://your-portainer.example.com +PORTAINER_API_KEY=your_api_key_here diff --git a/backend/index.js.bak b/backend/index.js.bak new file mode 100644 index 0000000..ee67420 --- /dev/null +++ b/backend/index.js.bak @@ -0,0 +1,154 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import https from 'https'; +import axios from 'axios'; +import http from 'http'; +import { Server } from 'socket.io'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +app.use(express.json()); + +// Statische Dateien +app.use(express.static(path.join(__dirname, 'public'))); + +// SPA-Fallback +app.get('*', (req, res, next) => { + if (req.path.startsWith('/api')) return next(); + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +const PORT = 4001; +const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); + +// HTTPS Agent +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios für Portainer +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +const redeployingStacks = {}; + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { cors: { origin: "*" } }); + +io.on("connection", (socket) => { + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); +}); + +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); +}; + +// --- API Endpoints --- + +// Stacks abrufen +app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; + }); + const uniqueStacks = Object.values(uniqueStacksMap); + + const stacksWithStatus = await Promise.all( + uniqueStacks.map(async (stack) => { + try { + const statusRes = await axiosInstance.get( + `/api/stacks/${stack.Id}/images_status?refresh=true` + ); + const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ [API] Fehler beim Abrufen Status Stack ${stack.Id}:`, err.message); + return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; + } + }) + ); + + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ [API] GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); + res.json(stacksWithStatus); + } catch (err) { + console.error(`❌ [API] Fehler beim Abrufen der Stacks:`, err.message); + res.status(500).json({ error: err.message }); + } +}); + +// Stack redeployen +app.put('/api/stacks/:id/redeploy', async (req, res) => { + const { id } = req.params; + console.log(`🔄 [API] PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); + + try { + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); + const stack = stackRes.data; + + if (stack.EndpointId !== ENDPOINT_ID) throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); + + if (stack.Type === 1) { + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); + await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); + } else if (stack.Type === 2) { + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); + const stackFileContent = fileRes.data?.StackFileContent; + if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); + + const services = fileRes.data?.Config?.services || {}; + for (const serviceName in services) { + const imageName = services[serviceName].image; + if (!imageName) continue; + try { + console.log(`🖼️ [Redeploy] Pulling image "${imageName}" für Service "${serviceName}"`); + await axiosInstance.post( + `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` + ); + } catch (err) { + console.error(`❌ [Redeploy] Fehler beim Pulling von Image "${imageName}":`, err.message); + } + } + + await axiosInstance.put(`/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + + broadcastRedeployStatus(id, false); + console.log(`✅ [API] PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); + res.json({ success: true, message: 'Stack redeployed' }); + } catch (err) { + broadcastRedeployStatus(id, false); + console.error(`❌ [API] Fehler beim Redeploy von Stack ${id}:`, err.message); + res.status(500).json({ error: err.message }); + } +}); + +// Server starten +server.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 [Server] Backend läuft auf Port ${PORT}`); +}); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 9560ebd..cc6c353 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,37 +6,40 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); - // WebSocket initialisieren useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); + const socket = io("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); - // Wenn Redeploy beendet, Stack-Status neu laden if (!status) { + // Status nach Redeploy neu vom Server holen try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.sort((a, b) => a.Name.localeCompare(b.Name))); } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + console.error("Fehler beim Aktualisieren nach Redeploy:", err); } + } else { + // UI direkt auf redeploying setzen + setStacks(prev => + prev.map(stack => + stack.Id === stackId ? { ...stack, redeploying: true } : stack + ) + ); } }); return () => socket.disconnect(); }, []); - // Stacks initial laden const fetchStacks = async () => { + setLoading(true); try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.map(stack => ({ ...stack, redeploying: stack.redeploying || false }))); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -49,15 +52,19 @@ export default function Stacks() { fetchStacks(); }, []); - // Redeploy eines Stacks const handleRedeploy = async (stackId) => { - setRedeploying(prev => ({ ...prev, [stackId]: true })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); try { await axios.put(`/api/stacks/${stackId}/redeploy`); + // Socket.IO Event aktualisiert Status automatisch } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - setRedeploying(prev => ({ ...prev, [stackId]: false })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); } }; @@ -67,8 +74,7 @@ export default function Stacks() { return (
{stack.Name}
ID: {stack.Id}
Lade Stacks...
; + if (error) return{error}
; + + return ( +{stack.Name}
+ID: {stack.Id}
+Keine Stacks gefunden.
} +Lade Stacks...
; - if (error) return{error}
; - - return ( -{stack.Name}
-ID: {stack.Id}
-Keine Stacks gefunden.
} -