diff --git a/admin.html b/admin.html index 78482c8..0e71eee 100644 --- a/admin.html +++ b/admin.html @@ -6,7 +6,7 @@ MischLabs Admin - +
@@ -65,6 +65,7 @@
Watchtower +
Noch nicht geladen.
@@ -80,6 +81,6 @@ - + diff --git a/admin.js b/admin.js index 25c4c31..cc85914 100644 --- a/admin.js +++ b/admin.js @@ -19,6 +19,7 @@ const saveLocalButton = document.querySelector('#saveLocalButton'); const resetLocalButton = document.querySelector('#resetLocalButton'); const downloadConfigButton = document.querySelector('#downloadConfigButton'); const refreshOpsButton = document.querySelector('#refreshOpsButton'); +const runWatchtowerButton = document.querySelector('#runWatchtowerButton'); const diskStatus = document.querySelector('#diskStatus'); const watchtowerStatus = document.querySelector('#watchtowerStatus'); const containerStatus = document.querySelector('#containerStatus'); @@ -404,6 +405,41 @@ async function restartContainer(row) { } } +async function runWatchtowerOnce() { + const confirmed = window.confirm('Watchtower jetzt manuell starten? Je nach Updates koennen Dienste kurz neu starten.'); + if (!confirmed) return; + + const previousText = runWatchtowerButton.textContent; + runWatchtowerButton.disabled = true; + runWatchtowerButton.textContent = 'Laeuft...'; + watchtowerStatus.innerHTML = '
Manueller LaufGestartet...
'; + + try { + const response = await fetch('/api/watchtower/run-once', { + method: 'POST', + headers: { + Authorization: `Bearer ${tokenSet.id_token}` + } + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`); + + watchtowerStatus.innerHTML = ` +
Manueller LaufGestartet
+
Container${payload.name}
+ `; + window.clearTimeout(opsRefreshTimer); + opsRefreshTimer = window.setTimeout(refreshOps, 25000); + } catch (error) { + watchtowerStatus.textContent = `Watchtower konnte nicht gestartet werden: ${error.message}`; + } finally { + window.setTimeout(() => { + runWatchtowerButton.disabled = false; + runWatchtowerButton.textContent = previousText; + }, 5000); + } +} + async function refreshOps() { diskStatus.textContent = 'Lade...'; watchtowerStatus.textContent = 'Lade...'; @@ -476,6 +512,7 @@ resetLocalButton.addEventListener('click', () => { downloadConfigButton.addEventListener('click', downloadConfig); refreshOpsButton.addEventListener('click', refreshOps); +runWatchtowerButton.addEventListener('click', runWatchtowerOnce); boot().catch((error) => { loginMessage.textContent = `Adminseite konnte nicht starten: ${error.message}`; diff --git a/server.js b/server.js index dfa5a1f..550cefb 100644 --- a/server.js +++ b/server.js @@ -11,6 +11,8 @@ const dockerPublicDir = path.join(__dirname, 'public'); const PUBLIC_DIR = fs.existsSync(dockerPublicDir) ? dockerPublicDir : __dirname; const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock'; const WATCHTOWER_CONTAINER = process.env.WATCHTOWER_CONTAINER || 'watchtower'; +const WATCHTOWER_RUN_IMAGE = process.env.WATCHTOWER_RUN_IMAGE || 'containrrr/watchtower'; +const WATCHTOWER_CONFIG_PATH = process.env.WATCHTOWER_CONFIG_PATH || '/volume2/docker/watchtower/config.json'; const AUTHORITY = process.env.AUTHORITY || 'https://auth.mischlabs.de/realms/mischlabs'; const ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'mischlabs-admin'; const ADMIN_USERS = (process.env.ADMIN_USERS || 'mrdiderot').split(',').map((user) => user.trim().toLowerCase()).filter(Boolean); @@ -74,7 +76,8 @@ function dockerRequest(endpoint, options = {}) { const req = http.request({ socketPath: DOCKER_SOCKET, path: endpoint, - method: options.method || 'GET' + method: options.method || 'GET', + headers: options.headers || {} }, (res) => { let body = ''; res.setEncoding('utf8'); @@ -284,6 +287,42 @@ async function restartContainer(containerName) { }; } +async function runWatchtowerOnce() { + const name = `mischlabs-watchtower-run-once-${Date.now()}`; + const body = JSON.stringify({ + Image: WATCHTOWER_RUN_IMAGE, + Cmd: ['--run-once'], + Labels: { + 'com.mischlabs.role': 'manual-watchtower-run', + 'com.centurylinklabs.watchtower.enable': 'false' + }, + HostConfig: { + AutoRemove: true, + Binds: [ + '/var/run/docker.sock:/var/run/docker.sock', + `${WATCHTOWER_CONFIG_PATH}:/config.json:ro` + ] + } + }); + + const created = await dockerRequest(`/containers/create?name=${encodeURIComponent(name)}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + }, + body + }); + + await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' }); + return { + id: created.Id.slice(0, 12), + name, + image: WATCHTOWER_RUN_IMAGE, + startedAt: new Date().toISOString() + }; +} + async function getDisks() { const disks = []; @@ -495,6 +534,21 @@ const server = http.createServer(async (req, res) => { return; } + if (req.url?.match(/^\/api\/watchtower\/run-once(?:[?#].*)?$/)) { + if (req.method !== 'POST') { + json(res, 405, { error: 'Method not allowed' }); + return; + } + + try { + await verifyAdminRequest(req); + json(res, 202, await runWatchtowerOnce()); + } catch (error) { + json(res, error.statusCode || 500, { error: error.message }); + } + return; + } + await serveStatic(req, res); });