Show manual Watchtower run status
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
This commit is contained in:
@@ -81,6 +81,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/admin.js?v=6" defer></script>
|
<script src="/admin.js?v=7" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
55
admin.js
55
admin.js
@@ -27,6 +27,8 @@ const containerStatus = document.querySelector('#containerStatus');
|
|||||||
let config = null;
|
let config = null;
|
||||||
let tokenSet = null;
|
let tokenSet = null;
|
||||||
let opsRefreshTimer = null;
|
let opsRefreshTimer = null;
|
||||||
|
let latestWatchtower = null;
|
||||||
|
let lastManualWatchtowerRun = null;
|
||||||
|
|
||||||
function base64UrlEncode(buffer) {
|
function base64UrlEncode(buffer) {
|
||||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
||||||
@@ -312,6 +314,23 @@ function renderDisks(disks = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderWatchtower(watchtower) {
|
function renderWatchtower(watchtower) {
|
||||||
|
latestWatchtower = watchtower;
|
||||||
|
if (lastManualWatchtowerRun) {
|
||||||
|
const run = lastManualWatchtowerRun;
|
||||||
|
const session = run.session;
|
||||||
|
const finished = run.finishedAt ? new Date(run.finishedAt).toLocaleString('de-DE') : 'laeuft noch';
|
||||||
|
const state = run.running ? 'Laeuft' : `Beendet (${run.exitCode ?? '--'})`;
|
||||||
|
watchtowerStatus.innerHTML = `
|
||||||
|
<div class="ops-line"><strong>Manueller Lauf</strong><span>${state}</span></div>
|
||||||
|
<div class="ops-line"><strong>Gestartet</strong><span>${run.startedAt ? new Date(run.startedAt).toLocaleString('de-DE') : '--'}</span></div>
|
||||||
|
<div class="ops-line"><strong>Fertig</strong><span>${finished}</span></div>
|
||||||
|
<div class="ops-line"><strong>Geprueft</strong><span>${session?.scanned ?? '--'}</span></div>
|
||||||
|
<div class="ops-line"><strong>Aktualisiert</strong><span>${session?.updated ?? '--'}</span></div>
|
||||||
|
<div class="ops-line"><strong>Fehler</strong><span>${session?.failed ?? (run.exitCode ? 1 : 0)}</span></div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const session = watchtower?.lastSession;
|
const session = watchtower?.lastSession;
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const tail = watchtower?.tail?.slice(-4)?.join(' | ');
|
const tail = watchtower?.tail?.slice(-4)?.join(' | ');
|
||||||
@@ -424,12 +443,20 @@ async function runWatchtowerOnce() {
|
|||||||
const payload = await response.json().catch(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
||||||
|
|
||||||
|
lastManualWatchtowerRun = {
|
||||||
|
id: payload.fullId || payload.id,
|
||||||
|
name: payload.name,
|
||||||
|
running: true,
|
||||||
|
startedAt: payload.startedAt,
|
||||||
|
session: null,
|
||||||
|
tail: []
|
||||||
|
};
|
||||||
watchtowerStatus.innerHTML = `
|
watchtowerStatus.innerHTML = `
|
||||||
<div class="ops-line"><strong>Manueller Lauf</strong><span>Gestartet</span></div>
|
<div class="ops-line"><strong>Manueller Lauf</strong><span>Gestartet</span></div>
|
||||||
<div class="ops-line"><strong>Container</strong><span>${payload.name}</span></div>
|
<div class="ops-line"><strong>Container</strong><span>${payload.name}</span></div>
|
||||||
`;
|
`;
|
||||||
window.clearTimeout(opsRefreshTimer);
|
window.clearTimeout(opsRefreshTimer);
|
||||||
opsRefreshTimer = window.setTimeout(refreshOps, 25000);
|
pollWatchtowerRun(payload.fullId || payload.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
watchtowerStatus.textContent = `Watchtower konnte nicht gestartet werden: ${error.message}`;
|
watchtowerStatus.textContent = `Watchtower konnte nicht gestartet werden: ${error.message}`;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -440,6 +467,32 @@ async function runWatchtowerOnce() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pollWatchtowerRun(runId, attempt = 0) {
|
||||||
|
try {
|
||||||
|
const cleanup = attempt > 0 ? '&cleanup=1' : '';
|
||||||
|
const response = await fetch(`/api/watchtower/runs/${encodeURIComponent(runId)}?${cleanup}`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokenSet.id_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
|
||||||
|
|
||||||
|
lastManualWatchtowerRun = payload;
|
||||||
|
renderWatchtower(latestWatchtower);
|
||||||
|
|
||||||
|
if (payload.running && attempt < 36) {
|
||||||
|
opsRefreshTimer = window.setTimeout(() => pollWatchtowerRun(runId, attempt + 1), 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opsRefreshTimer = window.setTimeout(refreshOps, 4000);
|
||||||
|
} catch (error) {
|
||||||
|
watchtowerStatus.textContent = `Manueller Watchtower-Lauf nicht lesbar: ${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshOps() {
|
async function refreshOps() {
|
||||||
diskStatus.textContent = 'Lade...';
|
diskStatus.textContent = 'Lade...';
|
||||||
watchtowerStatus.textContent = 'Lade...';
|
watchtowerStatus.textContent = 'Lade...';
|
||||||
|
|||||||
69
server.js
69
server.js
@@ -297,7 +297,7 @@ async function runWatchtowerOnce() {
|
|||||||
'com.centurylinklabs.watchtower.enable': 'false'
|
'com.centurylinklabs.watchtower.enable': 'false'
|
||||||
},
|
},
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
AutoRemove: true,
|
AutoRemove: false,
|
||||||
Binds: [
|
Binds: [
|
||||||
'/var/run/docker.sock:/var/run/docker.sock',
|
'/var/run/docker.sock:/var/run/docker.sock',
|
||||||
`${WATCHTOWER_CONFIG_PATH}:/config.json:ro`
|
`${WATCHTOWER_CONFIG_PATH}:/config.json:ro`
|
||||||
@@ -317,12 +317,61 @@ async function runWatchtowerOnce() {
|
|||||||
await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' });
|
await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' });
|
||||||
return {
|
return {
|
||||||
id: created.Id.slice(0, 12),
|
id: created.Id.slice(0, 12),
|
||||||
|
fullId: created.Id,
|
||||||
name,
|
name,
|
||||||
image: WATCHTOWER_RUN_IMAGE,
|
image: WATCHTOWER_RUN_IMAGE,
|
||||||
startedAt: new Date().toISOString()
|
startedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeContainer(id) {
|
||||||
|
await dockerRequest(`/containers/${encodeURIComponent(id)}?force=1`, { method: 'DELETE' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWatchtowerRun(runId, cleanup = false) {
|
||||||
|
const id = normalizeContainerName(runId);
|
||||||
|
if (!id || !/^[a-fA-F0-9]{12,64}$/.test(id)) {
|
||||||
|
const error = new Error('Ungueltige Watchtower-Run-ID.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [inspect, logs] = await Promise.all([
|
||||||
|
dockerRequest(`/containers/${encodeURIComponent(id)}/json`),
|
||||||
|
dockerLogs(id, 260).catch((error) => [`watchtower run logs unavailable: ${error.message}`])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const labels = inspect.Config?.Labels || {};
|
||||||
|
if (labels['com.mischlabs.role'] !== 'manual-watchtower-run') {
|
||||||
|
const error = new Error('Container gehoert nicht zu manuellen Watchtower-Laeufen.');
|
||||||
|
error.statusCode = 403;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = inspect.State || {};
|
||||||
|
const parsed = Array.isArray(logs) ? parseWatchtower(logs) : null;
|
||||||
|
const payload = {
|
||||||
|
id: inspect.Id.slice(0, 12),
|
||||||
|
fullId: inspect.Id,
|
||||||
|
name: normalizeContainerName(inspect.Name),
|
||||||
|
image: inspect.Config?.Image || WATCHTOWER_RUN_IMAGE,
|
||||||
|
state: state.Status || 'unknown',
|
||||||
|
running: Boolean(state.Running),
|
||||||
|
exitCode: state.ExitCode,
|
||||||
|
startedAt: state.StartedAt || null,
|
||||||
|
finishedAt: state.FinishedAt && !state.FinishedAt.startsWith('0001-') ? state.FinishedAt : null,
|
||||||
|
session: parsed,
|
||||||
|
tail: Array.isArray(logs) ? logs.slice(-20) : logs
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cleanup && !payload.running) {
|
||||||
|
removeContainer(inspect.Id).catch(() => {});
|
||||||
|
payload.cleanedUp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
async function getDisks() {
|
async function getDisks() {
|
||||||
const disks = [];
|
const disks = [];
|
||||||
|
|
||||||
@@ -549,6 +598,24 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const watchtowerRunMatch = req.url?.match(/^\/api\/watchtower\/runs\/([^/?#]+)(?:[?#].*)?$/);
|
||||||
|
if (watchtowerRunMatch) {
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
json(res, 405, { error: 'Method not allowed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await verifyAdminRequest(req);
|
||||||
|
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const cleanup = requestUrl.searchParams.get('cleanup') === '1';
|
||||||
|
json(res, 200, await getWatchtowerRun(decodeURIComponent(watchtowerRunMatch[1]), cleanup));
|
||||||
|
} catch (error) {
|
||||||
|
json(res, error.statusCode || 500, { error: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await serveStatic(req, res);
|
await serveStatic(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user