Clean up manual Watchtower containers
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s

This commit is contained in:
Kroonk
2026-05-21 17:07:28 +02:00
parent c3ac1be083
commit 5b252f17fd
3 changed files with 71 additions and 13 deletions

View File

@@ -81,6 +81,6 @@
</section> </section>
</main> </main>
<script src="/admin.js?v=7" defer></script> <script src="/admin.js?v=8" defer></script>
</body> </body>
</html> </html>

View File

@@ -469,8 +469,7 @@ async function runWatchtowerOnce() {
async function pollWatchtowerRun(runId, attempt = 0) { async function pollWatchtowerRun(runId, attempt = 0) {
try { try {
const cleanup = attempt > 0 ? '&cleanup=1' : ''; const response = await fetch(`/api/watchtower/runs/${encodeURIComponent(runId)}`, {
const response = await fetch(`/api/watchtower/runs/${encodeURIComponent(runId)}?${cleanup}`, {
cache: 'no-store', cache: 'no-store',
headers: { headers: {
Authorization: `Bearer ${tokenSet.id_token}` Authorization: `Bearer ${tokenSet.id_token}`

View File

@@ -62,6 +62,7 @@ const serviceUrls = [
let jwksCache = null; let jwksCache = null;
let jwksCacheUntil = 0; let jwksCacheUntil = 0;
const manualWatchtowerRuns = new Map();
function json(res, status, payload) { function json(res, status, payload) {
res.writeHead(status, { res.writeHead(status, {
@@ -288,6 +289,8 @@ async function restartContainer(containerName) {
} }
async function runWatchtowerOnce() { async function runWatchtowerOnce() {
cleanupManualWatchtowerContainers().catch(() => {});
const name = `mischlabs-watchtower-run-once-${Date.now()}`; const name = `mischlabs-watchtower-run-once-${Date.now()}`;
const body = JSON.stringify({ const body = JSON.stringify({
Image: WATCHTOWER_RUN_IMAGE, Image: WATCHTOWER_RUN_IMAGE,
@@ -315,20 +318,54 @@ async function runWatchtowerOnce() {
}); });
await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' }); await dockerRequest(`/containers/${created.Id}/start`, { method: 'POST' });
return { const payload = {
id: created.Id.slice(0, 12), id: created.Id.slice(0, 12),
fullId: created.Id, fullId: created.Id,
name, name,
image: WATCHTOWER_RUN_IMAGE, image: WATCHTOWER_RUN_IMAGE,
startedAt: new Date().toISOString() state: 'running',
running: true,
exitCode: null,
startedAt: new Date().toISOString(),
finishedAt: null,
session: null,
tail: []
}; };
manualWatchtowerRuns.set(created.Id, payload);
monitorWatchtowerRun(created.Id).catch((error) => {
manualWatchtowerRuns.set(created.Id, {
...payload,
state: 'error',
running: false,
exitCode: 1,
finishedAt: new Date().toISOString(),
error: error.message
});
});
return payload;
} }
async function removeContainer(id) { async function removeContainer(id) {
await dockerRequest(`/containers/${encodeURIComponent(id)}?force=1`, { method: 'DELETE' }); await dockerRequest(`/containers/${encodeURIComponent(id)}?force=1`, { method: 'DELETE' });
} }
async function getWatchtowerRun(runId, cleanup = false) { async function waitContainer(id) {
return dockerRequest(`/containers/${encodeURIComponent(id)}/wait`, { method: 'POST' });
}
async function cleanupManualWatchtowerContainers() {
const containers = await dockerRequest('/containers/json?all=1');
await Promise.all((containers || [])
.filter((container) => (
container.Labels?.['com.mischlabs.role'] === 'manual-watchtower-run'
&& container.State !== 'running'
))
.map((container) => removeContainer(container.Id).catch(() => {})));
}
async function readWatchtowerRunContainer(runId) {
const id = normalizeContainerName(runId); const id = normalizeContainerName(runId);
if (!id || !/^[a-fA-F0-9]{12,64}$/.test(id)) { if (!id || !/^[a-fA-F0-9]{12,64}$/.test(id)) {
const error = new Error('Ungueltige Watchtower-Run-ID.'); const error = new Error('Ungueltige Watchtower-Run-ID.');
@@ -363,13 +400,34 @@ async function getWatchtowerRun(runId, cleanup = false) {
session: parsed, session: parsed,
tail: Array.isArray(logs) ? logs.slice(-20) : logs tail: Array.isArray(logs) ? logs.slice(-20) : logs
}; };
}
if (cleanup && !payload.running) { async function monitorWatchtowerRun(runId) {
removeContainer(inspect.Id).catch(() => {}); await waitContainer(runId).catch(() => null);
payload.cleanedUp = true;
const payload = await readWatchtowerRunContainer(runId);
manualWatchtowerRuns.set(runId, payload);
await removeContainer(runId).catch(() => {});
}
async function getWatchtowerRun(runId) {
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;
} }
return payload; if (manualWatchtowerRuns.has(id)) {
return manualWatchtowerRuns.get(id);
}
try {
return await readWatchtowerRunContainer(id);
} catch (error) {
error.statusCode = error.statusCode || 404;
throw error;
}
} }
async function getDisks() { async function getDisks() {
@@ -607,9 +665,7 @@ const server = http.createServer(async (req, res) => {
try { try {
await verifyAdminRequest(req); await verifyAdminRequest(req);
const requestUrl = new URL(req.url, `http://${req.headers.host}`); json(res, 200, await getWatchtowerRun(decodeURIComponent(watchtowerRunMatch[1])));
const cleanup = requestUrl.searchParams.get('cleanup') === '1';
json(res, 200, await getWatchtowerRun(decodeURIComponent(watchtowerRunMatch[1]), cleanup));
} catch (error) { } catch (error) {
json(res, error.statusCode || 500, { error: error.message }); json(res, error.statusCode || 500, { error: error.message });
} }
@@ -621,4 +677,7 @@ const server = http.createServer(async (req, res) => {
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`MischLabs dashboard listening on ${PORT}`); console.log(`MischLabs dashboard listening on ${PORT}`);
cleanupManualWatchtowerContainers().catch((error) => {
console.warn(`Manual Watchtower cleanup failed: ${error.message}`);
});
}); });